Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# pattern matching on records with optional fields

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?

like image 607
Giles Avatar asked Jan 22 '15 13:01

Giles


2 Answers

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).

like image 144
pad Avatar answered Nov 08 '22 15:11

pad


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 )
like image 25
kaefer Avatar answered Nov 08 '22 16:11

kaefer