making `option` (and more) useable with `yield!` in computation expressions
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 ...
- custom Computation Expression
optionSeq(and others for other data structures) with overloads - changing
Option(and other relevant value containers) type to implementIEnumerable
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:
- makes it much terser to deconstruct option (or value container like) types in context of yielding values in a computation expression
- more 0 cost abstraction
The disadvantages of making this adjustment to F# are:
- introducing of concept that doesn't align with how
yield!currently works (only withIEnumerableAFAIU) - may look surprising if the construct is not documented in coding guidelines nor around contents explaining
yield! - may imply it can be used in
forconstruct (which shouldn't be the case in context of this sole feature and #716)
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:
- [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
- [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
- [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.
Please tick all that apply:
- [x] This is not a breaking change to the F# language design
- [ ] I or my company would be willing to help implement and/or test this
- [x] I am willing to contribute to adjusting relevant coding guidelines if this gets into an approved and implemented state
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.
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.
Isn't
yield! Option.toList data.b
already a way of doing this?