Search code examples
f#f#-3.0computation-expressionquery-expressions

F# query expressions yield


I am learning F# and now am reading about computation expressions and query expressions to use with SQL type providers. I was doing some simple tasks and at some point needed to concatenate (Union) 2 queries, my first thought, after reading about yield in sequences and lists was to do the same inside a query expression like this:

query {
    yield! for a in db.As select { // projection }
    yield! for b in db.Bs select { // projection }
}

this was invalid code, then my second approach was to 'concat' them using:

seq {
    yield! query {...}
    yield! query {...}
}

or using the Linq's Concat function like this: (query {...}).Concat(query {...}). How to do it came from this question's answer

Both of the above approaches work with one difference though, using seq will run 2 SQL queries, and the Concat runs just one which is understandable.

My question then is: why isn't yield supported on query expressions?


EDIT:

After further investigation I got to the MSDN docs and i saw the Yield and YieldFrom methods implemented, but not the Combine and Delay methods, which is even more confusing for me now


Solution

  • yield! is supported to some extent in queries, and can be used where select normally is:

    query { 
        for x in [5;2;0].AsQueryable() do 
        where (x > 1)
        sortBy x
        yield! [x; x-1] 
     } |> Seq.toList // [2;1;5;4]
    

    However, in general you can't arbitrarily intersperse query and sequence operations, because it would be hard to define how they should compose:

    query {
        for x in [1;2;3] do
        where (x > 1)
        while true do // error: can't use while (what would it mean?)
        sortBy x 
    }
    

    Likewise:

    query {
        for x in [1;2;3] do
        where (x > 1)
        sortBy x 
        yield! ['a';'b';'c']
        yield! ['x';'y';'z'] // error
    }        
    

    This is kind of ambiguous because it's not clear whether the second yield! is inside the for loop or is appending a set of elements afterwards.

    So it's best to think of queries as queries and sequences as sequences, even though both kinds of computation expressions support some of the same operations.

    Generally, query custom operators work element-wise, so expressing things like unions or concatenations are awkward because they deal with entire collections rather than individual elements. But if you wanted to, you can create a query builder that added a concat custom operator that took a sequence, though it might feel a bit asymmetrical:

    open System.Linq
    
    type QB() =
        member inline x.Yield v = (Seq.singleton v).AsQueryable()
        member inline x.YieldFrom q = q
        [<CustomOperation("where", MaintainsVariableSpace=true)>]
        member x.Where(q:IQueryable<_>, [<ProjectionParameter>]c:Expressions.Expression<System.Func<_,_>>) = q.Where(c)
        [<CustomOperation("sortBy", MaintainsVariableSpace=true)>]
        member x.SortBy(q:IQueryable<_>, [<ProjectionParameter>]c:Expressions.Expression<System.Func<_,_>>) = q.OrderBy(c)
        [<CustomOperation("select")>]
        member x.Select(q:IQueryable<_>, [<ProjectionParameter>]c:Expressions.Expression<System.Func<_,_>>) = q.Select(c)
        [<CustomOperation("concat")>]
        member x.Concat(q:IQueryable<_>, q') = q.Concat(q')
        member x.For(q:IQueryable<'t>, c:'t->IQueryable<'u>) = q.SelectMany(fun t -> c t :> seq<_>) // TODO: implement something more reasonable here
    
    let qb = QB()
    
    qb {
        for x in ([5;2;0].AsQueryable()) do
        where (x > 1)
        sortBy x
        select x
        concat ([7;8;9].AsQueryable())
    } |> Seq.toList