Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate union case remapping common in monadic functions #15872

Open
kerams opened this issue Aug 26, 2023 · 7 comments
Open

Eliminate union case remapping common in monadic functions #15872

kerams opened this issue Aug 26, 2023 · 7 comments
Labels
Milestone

Comments

@kerams
Copy link
Contributor

kerams commented Aug 26, 2023

Consider an inlined version of Result.map

let inline map mapping result =
    match result with
    | Error e -> Error e
    | Ok x -> Ok (mapping x)

let ff x = map (fun y -> y.ToString ()) x

let ffs x = map id x

The last 2 functions compiled down to

public static FSharpResult<string, b> ff<a, b>(FSharpResult<a, b> x)
{
    if (x.Tag == 0)
    {
        a resultValue = x.ResultValue;
        a a = resultValue;
        return FSharpResult<string, b>.NewOk(a.ToString());
    }
    return FSharpResult<string, b>.NewError(x.ErrorValue);
}

public static FSharpResult<a, b> ffs<a, b>(FSharpResult<a, b> x)
{
    if (x.Tag == 0)
    {
        return FSharpResult<a, b>.NewOk(x.ResultValue);
    }
    return FSharpResult<a, b>.NewError(x.ErrorValue);
}

In both functions a new error case is allocated. This is necessary in ff (and in the original map), because the input result type is Result<'a, 'b> and the output is Result<string, 'b>. However, input and output types are the same in ffs, so the original union case can simply pass through instead of being recreated.

public static FSharpResult<a, b> ffs<a, b>(FSharpResult<a, b> x)
{
    if (x.Tag == 0)
    {
        return FSharpResult<a, b>.NewOk(x.ResultValue);
    }
    return x; // <---------
}
@kerams
Copy link
Contributor Author

kerams commented Aug 26, 2023

I've had a look at how this could be implemented in Optimizer.fs. Unfortunately, it isn't immediately obvious that a union construction expression arises from a match clause like | Error e -> Error e, because the original match expression undergoes significant changes during pattern compilation. In theory we could always replace a union construction expression in the form Case (old.Field1, old.Field2) provided that the type of this expression and old is the same and all fields are supplied from old in the right order.

This would mean that even

let copy = Case (old.Field1, old.Field2)

would be turned into

let copy = old

which is perfectly OK thanks to union immutability, but is problematic when someone relies on reference (in)equality.

@Happypig375
Copy link
Member

@kerams Reference equality is already an issue in your original optimisation.

@kerams
Copy link
Contributor Author

kerams commented Aug 26, 2023

Indeed, but you have no choice - | e -> e doesn't fly, even though the author might have liked that.

@Happypig375
Copy link
Member

@kerams or you can investigate making that syntax work.

@Happypig375
Copy link
Member

If that syntax can work, then more complex cases of returning the original may also work:

let inline map mapping result =
    match result with
    | Ok x -> Ok (mapping x)
    | (Error e) as x ->
        printfn $"This error is {e}"
        x

@kerams
Copy link
Contributor Author

kerams commented Aug 26, 2023

That does work already and constrains mapping to 'a -> 'a.

@Happypig375
Copy link
Member

Ah then this path won't work. Nevermind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: New
Development

No branches or pull requests

3 participants