5

The documentation for leftOuterJoin Query Expressions on MSDN repeatedly implies through the samples that when using leftOuterJoin .. on .. into .. that you must still use .DefaultIfEmpty() to achieve the desired effect.

I don't believe this is necessary because I get the same results in both of these tests which differ only in that the second one does not .DefaultIfEpmty()

type Test = A | B | C
let G = [| A; B; C|]
let H = [| A; C; C|]

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I.DefaultIfEmpty() do 
    select (g, i)}

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I do 
    select (g, i)}

// seq [(A, A); (B, null); (C, C); (C, C)]
// seq [(A, A); (B, null); (C, C); (C, C)]

1) Can you confirm this?

If that's right, I realized it only after writing this alternate type augmentation in an attempt to better deal with unmatched results and I was surprised to still see nulls in my output!

type IEnumerable<'TSource> with
    member this.NoneIfEmpty = if (Seq.exists (fun _ -> true) this) 
                              then Seq.map (fun e -> Some e) this 
                              else seq [ None ]

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I.NoneIfEmpty do 
    select (g, i)}

// seq [(A, Some A); (B, Some null); (C, Some C); (C, Some C)]

2) Is there a way to get None instead of null/Some null from the leftOuterJoin?

3) What I really want to do is find out if there are any unmatched g

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I.NoneIfEmpty do
    where (i.IsNone)
    exists (true) }

I figured this next one out but it isn't very F#:

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I do
    where (box i = null) 
    exists (true)}
Jason Kleban
  • 20,024
  • 18
  • 75
  • 125

1 Answers1

5

Short version: Query Expressions use nulls. It's a rough spot in the language, but a containable one.

I've done this before:

let ToOption (a:'a) =
    match obj.ReferenceEquals(a,null) with
    | true -> None
    | false -> Some(a)

This will let you do:

printfn "%A" <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I do 
    select ( g,(ToOption i))}

Which wraps every result in an option (since you don't know if there is going to be an I. It's worth noting that F# uses null to represent None at run-time as an optimization. So to check if this is indeed what you want, make a decision on the option, like:

Seq.iter (fun (g,h) -> 
              printf "%A," g; 
              match h with 
              | Some(h) -> printfn "Some (%A)" h 
              | None -> printfn "None")  
    <| query {
    for g in G do
    leftOuterJoin h in H on (g = h) into I
    for i in I do 
    select ((ToOption g),(ToOption i))}
Christopher Stevenson
  • 2,843
  • 20
  • 25
  • 1
    Awesome, thanks. And so back to 1) is the documentation truly flawed? I'd put in for it. – Jason Kleban Sep 24 '14 at 10:35
  • Hmm... what do you think should be the behavior? – Christopher Stevenson Sep 24 '14 at 12:50
  • 1
    After doing a bit of research into [DefaultIfEmpty](http://msdn.microsoft.com/en-us/library/vstudio/bb355419(v=vs.110).aspx), it does nothing since `I` is flattened. – Christopher Stevenson Sep 24 '14 at 13:15
  • Yeah, in all cases, right? I mean, the C# LINQ way to do a left join is with a `join .. into .. from ...DefaultIfEmtpy()` so I would think that the only reason to have both a `join` and a `leftOuterJoin` keyword here is so you don't have to both with that nonsense. And regardless of the provider, I would think that an empty set is an empty set. I don't know what could change about the provider that would affect this (not that I'm qualified to recognize it). – Jason Kleban Sep 24 '14 at 13:51