With leftOuterJoin, .DefaultIfEmpty() is unnecessary
Asked Answered
C

1

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)}
Carangid answered 24/9, 2014 at 3:30 Comment(0)
S
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))}
Supernova answered 24/9, 2014 at 4:55 Comment(4)
Awesome, thanks. And so back to 1) is the documentation truly flawed? I'd put in for it.Carangid
Hmm... what do you think should be the behavior?Supernova
After doing a bit of research into DefaultIfEmpty, it does nothing since I is flattened.Supernova
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).Carangid

© 2022 - 2024 — McMap. All rights reserved.