fsharp / fslang-suggestions

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

making `option` (and more) useable with `yield!` in computation expressions #1293

Open smoothdeveloper opened 1 year ago

smoothdeveloper commented 1 year ago
type Data = {
  a: string
  b: string option
  c: string option
}

let values data =
  seq {
    data.a
    yield! data.b
    yield! data.c
  }

I'd like usage of yield! in values to be syntax sugar (0 cost abstraction) for

let values data =
  seq {
    data.a
    match data.b with
    | None -> ()
    | Some b -> b
    match data.c with
    | None -> ()
    | Some c -> c
  }

I'd also like to figure if this can be made possible on types without them going to extent of implementing iterator patterns, applying to System.Nullable,ValueOption, Result would make sense in terms of FSharp.Core / compiler implementation if the suggestion is appealing.

It could be something that is in coding guidelines for types that implement map, where the semantic makes sense, if the suggestion can be taken from standpoint of enabling this on adhoc type (which would be great).

The existing way of approaching this problem in F# is ...

I'm not sure the alternatives can bring it to a 0 cost abstraction.

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): between S and L depending on wether we just want this for option/voption/nullable and not supporting the same on adhoc types.

Related suggestions: (#716)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

SchlenkR commented 1 year ago

An idea how this could be implemented in a library if we had a SeqBuilder:


// Hint on yield!
// ---------------
// The usage of yield! looks non-idiomatic,
// since it usually is used to yield the computation's base type
// (e.g. seq<'a>), and now it's intention is extended to 
// yield a "seq-like" value option<'a>.

// Why yield! has to be used in any case:
// ------------------------------------------
// yield can't be used here (so I used yield!, even though
// it might seem unfamiliar in the first place - see comment above)
// because of ambiguities in overloads.
// In other words: All yielded values must match exactly
// one overload, which would not be the case having 2 yield-overloads
// with 'a and 'a option (both would match yielding an 'a option,
// even though one is more specific that the other).

// Why this sample can't work with the `seq` implementation:
// -----------------------------------------------------------------
// As far as I know, there's no SeqBuilder in F# core that could
// be used to augment (it's baked into the compiler),
// so this is a non-optimized custom SeqBuilder for
// demo purposes only.

type SeqBuilder() =
    member _.For(m: seq<_>, f) = Seq.collect f m
    member _.Yield(v) = Seq.singleton v
    member _.Zero<'x>() = Seq.empty<'x>
    member _.Combine(a, delayedB) = Seq.append a (delayedB ())
    member _.Delay(f) = f
    member _.Run(f) = f()

    // here we go...
    member _.YieldFrom(m: option<_>) =
        match m with
        | Some v -> Seq.singleton v
        | None -> Seq.empty

let seq = SeqBuilder()

// -------------------------------------------

type Data = {
    a: string
    b: string option
    c: string option
}

let getValues data =
    seq {
        data.a
        yield! data.b
        yield! data.c
    }

getValues { a = "a"; b = Some "b"; c = None }
// val it: seq ["a"; "b"]

A way of approaching a production ready solution could be to implement a performant seq builder utilizing "resumable code", or by perf-tuning a "normal" builder with InlineIfLambda and other techniques, then making that builder available in a library and shadow the seq keyword.

charlesroddie commented 1 year ago

Is there a reason to have yield! but not for ... in?

I was thinking of creating an issue for ValueOption<'t> to support IEnumerable<'t> (which doesn't have the null problem supporting it that Option<'t> has). But the main use cases would be for ... in and yield! so if those are implemented then it wouldn't be needed.

MaxWilson commented 1 year ago

Isn't

yield! Option.toList data.b

already a way of doing this?