F#'s 'options' seem a nice way of using the type system to separate data that's known to be present from data which may or may not be present, and I like the way that the match
expression enforces that all cases are considered:
match str with
| Some s -> functionTakingString(s)
| None -> "abc" // The compiler would helpfully complain if this line wasn't present
It's very useful that s
(opposed to str
) is a string
rather than a string option
.
However, when working with records that have optional fields...
type SomeRecord =
{
field1 : string
field2 : string option
}
...and those records are being filtered, a match
expression feels unnecessary, because there's nothing sensible to do in the None
case, but this...
let functionTakingSequenceOfRecords mySeq =
mySeq
|> Seq.filter (fun record -> record.field2.IsSome)
|> Seq.map (fun record -> functionTakingString field2) // Won't compile
... won't compile, because although records where field2
isn't populated have been filtered out, the type of field2
is still string option
, not string
.
I could define another record type, where field2
isn't optional, but that approach seems complicated, and may be unworkable with many optional fields.
I've defined an operator that raises an exception if an option is None
...
let inline (|?!) (value : 'a option) (message : string) =
match value with
| Some x -> x
| None -> invalidOp message
...and have changed the previous code to this...
let functionTakingSequenceOfRecords mySeq =
mySeq
|> Seq.filter (fun record -> record.field2.IsSome)
|> Seq.map (fun record -> functionTakingString (record.field2 |?! "Should never happen")) // Now compiles
...but it doesn't seem ideal. I could use Unchecked.defaultof
instead of raising an exception, but I'm not sure that's any better. The crux of it is that the None
case isn't relevant after filtering.
Are there any better ways of handling this?
EDIT
The very interesting answers have brought record pattern matching to my attention, which I wasn't aware of, and Value
, which I'd seen but misunderstood (I see that it throws a NullReferenceException
if None
). But I think my example may have been poor, as my more complex, real-life problem involves using more than one field from the record. I suspect I'm stuck with something like...
|> Seq.map (fun record -> functionTakingTwoStrings record.field1 record.field2.Value)
unless there's something else?
In this example, you could use:
let functionTakingSequenceOfRecords mySeq =
mySeq
|> Seq.choose (fun { field2 = v } -> v)
|> Seq.map functionTakingString
Seq.choose
allows us to filter items based on optional results. Here we you pattern matching on records for more concise code.
I think the general idea is to manipulate option values using combinators, high-order functions until you would like to transform them into values of other types (e.g. using Seq.choose
in this case). Using your |?!
is discouraged because it is a partial operator (throwing exceptions in some cases). You can argue that it's safe to use in this particular case; but F# type system can't detect it and warn you about unsafe use in any case.
On a side note, I would recommend to take a look at Railway-Oriented Programming series at http://fsharpforfunandprofit.com/posts/recipe-part2/. The series show you type-safe and composable ways to handle errors where you can keep diagnostic information along.
UPDATE (upon your edit):
A revised version of your function is written as follows:
let functionTakingSequenceOfRecords mySeq =
mySeq
|> Seq.choose (fun { field1 = v1; field2 = v2 } ->
v2 |> Option.map (functionTakingString v1))
It demonstrates the general idea I mentioned where you manipulate option values using high-order functions (Option.map
) and transform them at a final step (Seq.choose
).
Since you have found the IsSome
property, you might have seen the Value
property as well.
let functionTakingSequenceOfRecords mySeq =
mySeq
|> Seq.filter (fun record -> record.field2.IsSome)
|> Seq.map (fun record -> functionTakingString record.field2.Value )
There's an alternative with pattern matching:
let functionTakingSequenceOfRecords' mySeq =
mySeq
|> Seq.choose (function
| { field2 = Some v } -> functionTakingString v |> Some
| _ -> None )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With