jinaga / jinaga.net

.NET immutable data management library
MIT License
14 stars 3 forks source link

Any tips for getting F# working? #137

Open njlr opened 1 month ago

njlr commented 1 month ago

I tried playing with the example code:

#r "nuget: Jinaga, 1.0.6"

open System.Linq
open Jinaga

[<FactType("Corporate.Company")>]
type Company =
  {
    Identifier : string
  }

[<FactType("Corporate.Employee")>]
type Employee =
  {
    Company : Company
    EmployeeNumber : int
  }

let employeesOfCompany =
  Given<Company>.Match(
    (fun (company : Company) (facts : Repository.FactRepository) ->
      facts.OfType<Employee>().Where(fun x -> x.Company = company))
  )

let j = JinagaClient.Create()

let employees = j.Query(employeesOfCompany, { Identifier = "Contoso" }).Result

for employee in employees do
  printfn $"%A{employee}"

But when I run it I hit a run-time error:

System.ArgumentException: Expected a unary lambda expression for QuotationToLambdaExpression(SubstHelper(NewDelegate (Func`2, x, x), new [] {}, new [] {})).
   at Jinaga.Repository.SpecificationProcessor.GetLambda(Expression argument)
   at Jinaga.Repository.SpecificationProcessor.ProcessSource(Expression expression, SymbolTable symbolTable, String recommendedLabel)
   at Jinaga.Repository.SpecificationProcessor.Queryable[TProjection](LambdaExpression specExpression)
   at Jinaga.Given`1.Match[TProjection](Expression`1 specExpression)

I'm not at all familiar with Expression so unsure where I went wrong here.

Any ideas?

(You can paste this into an fsx file then run with dotnet fsi ./Repro.fsx)

michaellperry commented 1 month ago

That's awesome! I love how this looks in F#.

Let me work on this for a few days. I think I can extend the expression parser to accept the tree that F# produces.

If you want to give it a try yourself, clone the project and put a breakpoint in Jinaga/Repository/SpecificationProcessor.cs in the GetLambda method. Then run the unit test in Jinaga.Test/Specifications/SpecificationTest.cs called CanSpecifySuccessors. Once you hit that breakpoint and have a look around, you can write an F# version of the unit test. See what's different.

I'll let you know what I find.

michaellperry commented 1 month ago

When I got the code in the debugger, I found that the expression had the following structure:

.Call System.Linq.Queryable.Where(
    .Call $facts.OfType(),
    .Call Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToLambdaExpression(
        .Call Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.SubstHelper(
            .Constant<Microsoft.FSharp.Quotations.FSharpExpr>(NewDelegate (Func`2, x,
             Call (None, op_Equality,
                   [PropertyGet (Some (x), Company, []), company]))),
            .NewArray Microsoft.FSharp.Quotations.FSharpVar[] {
                .Constant<Microsoft.FSharp.Quotations.FSharpVar>(company)
            },
            .NewArray System.Object[] {
                (System.Object)$company
            }
        )
    )
)

It looks like there are ways to turn F# quotations into C# expressions, but I'm not familiar with them. For example F# Quotation Evaluator. This might take a while.

michaellperry commented 1 month ago

Pushed branch fsharp-tests

michaellperry commented 1 month ago

Making progress! The following test passes:

[<Fact>]
let CanSpecifyIdentity() =
    let specification = Given<Airline>.Select(
        <@ Func<Airline, FactRepository, Airline>(fun airline facts -> airline) @>
    )
    Assert.Equal(
        """(airline: Skylane.Airline) {
} => airline
""",
        specification.ToString().ReplaceLineEndings()
    )

I must clarify that the Given<T>.Select used in that test is not the one published by the C# library. I needed to express types in F# that have no C# equivalent in order to make that interface work. So I wrote a small wrapper in F#.

I hope I can get rid of the Func<TFact, FactRepository, TProjection> syntax. Projections tend to be complex types. It would be better to let type inference figure that out.

Update pushed to the fsharp-tests branch.

michaellperry commented 1 month ago

It turns out that F# will translate the Expr to Expression in this specific case. The syntax is much simplified, the projection type is inferred, and this test passes with the regular Given<T>.Select:

[<Fact>]
let CanSpecifyIdentity() =
    let specification = Given<Airline>.Select(
        fun airline facts -> airline
    )
    Assert.Equal(
        """(airline: Skylane.Airline) {
} => airline
""",
        specification.ToString().ReplaceLineEndings()
    )
michaellperry commented 1 month ago

It appears that the core issue is that F# only translates the initial Expr, not nested Exprs. This is apparently a fairly common complaint with respect to F# and ORMs.

It looks like the direction that the F# language is taking is to use query syntax. So I think we should focus on making this syntax work:

let employeesOfCompany =
    Given<Company>.Match<Employee>(
        fun company facts ->
            query {
                for employee in facts.OfType<Employee>() do
                where (employee.Company = company)
                select employee
            }
    )

What are your thoughts?

njlr commented 1 month ago

It appears that the core issue is that F# only translates the initial Expr, not nested Exprs. This is apparently a fairly common complaint with respect to F# and ORMs.

It looks like the direction that the F# language is taking is to use query syntax. So I think we should focus on making this syntax work:

let employeesOfCompany =
    Given<Company>.Match<Employee>(
        fun company facts ->
            query {
                for employee in facts.OfType<Employee>() do
                where (employee.Company = company)
                select employee
            }
    )

What are your thoughts?

This does look nice, and it mirrors the C# version too :+1: