fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
346 stars 21 forks source link

Allow use Expression<> and/or Func<> on Bind / For computations #1298

Open lucasteles opened 1 year ago

lucasteles commented 1 year ago

Looks like today to create custom computation expressions it is required that the bind function to be an FSharpFunc, allowing it to be Expression<> or Func<> could be very handy for CLR interop

An example is creating a query builder for EF

type EFQueryBuilder() =

    // don't work
    member _.For(m: IQueryable<'t>, f: Expression<Func<'t, IEnumerable<'u>>>) : IQueryable<'u> = m.SelectMany(f) 

    // works but create an wrapping expression on FSharpFunc
    // member _.For(m: IQueryable<'t>, f: 't -> IEnumerable<'u>) : IQueryable<'u> = m.SelectMany(f) 

    member _.Yield(q) = Enumerable.Repeat(q, 1)
    member _.YieldFrom(q: IQueryable<_>) = q
    member _.Zero() = Enumerable.Empty()

    [<CustomOperation("select", AllowIntoPattern = true)>]
    member _.Select(m: IQueryable<'t>, [<ProjectionParameter>] f: Expression<Func<'t, 'u>>) : IQueryable<'u> =
        m.Select(f)

let test =
        EFQueryBuilder() {
            for n in items do // ERROR: This function takes too many arguments, or is used in a context where a function is not expected 
                select $"{n}"
        }

And actually the custom operator with ProjectionParameter recognize the expression as the same as any function

Pros and Cons

The advantages of making this adjustment to F# are :

The disadvantages of making this adjustment to F# are:

Extra information

Estimated cost (XS, S, M, L, XL, XXL): ?

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

ijklam commented 11 months ago

You can make the single table query working like this

type EFQueryBuilder() = 
    member _.For (source: IQueryable<'T>, body: 'T -> _) : IQueryable<'T> =
        source

    [<CustomOperation("select",MaintainsVariableSpace=true,AllowIntoPattern=true)>] 
    member _.Select (source, [<ProjectionParameter>] projection: Expression<Func<'T,'U>>) =
        Queryable.Select(source, projection)

    [<CustomOperation("where",MaintainsVariableSpace=true)>] 
    member _.Where (source, [<ProjectionParameter>] projection: Expression<Func<'T,_>>) =
        Queryable.Where(source, projection)

    [<CustomOperation("orderBy",MaintainsVariableSpace=true)>] 
    member _.OrderBy (source, [<ProjectionParameter>] keySelector: Expression<Func<'T,_>>) =
        Queryable.OrderBy(source, keySelector)

    member _.Yield (value: 't) =
        Seq.singleton value

EFQueryBuilder() {
    for i in db.Blogs do
        where (i.BlogId > 1)
        select $"{i}"
}