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
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