In the last post I compared the new Nullable reference types that were introduced in C# 8 with using Option from language-ext, which tries to mimic the functional paradigm approach to representing the abscence of a value. One of the major drawbacks of Option in C# is that there is no built in language-support, instead you have to rely on third party packages, or roll your own.
In F# however, as well as in other functional languages, Option is built in from the start and is the default way to represent a missing value. I usually don’t write code in F# but I wanted to take a look on how the scenario I used in the previous post would look like in a functional-first language so I made a test implementation in F#. Now this is just dummy code to test the scenario, in a real world application the functions would look a lot different:
open System
type UserId = UserId of uint32
type Customer =
{ Id: UserId }
type Email = Email of string
let tryParse (s: string) : UserId option =
match UInt32.TryParse s with
| true, num -> Some (UserId num)
| _ -> None
let getCustomerById (id: UserId) : Customer option =
match id with
| UserId 1u -> Some { Id = UserId 1u }
| UserId 2u -> Some { Id = UserId 2u }
| _ -> None
let getEmailForCustomer (customer: Customer) : Email option =
match customer.Id with
| UserId 1u -> Some (Email "apan@bepan.se")
| _ -> None
let sendPromotionalEmail (email: Email) : unit =
()
[<EntryPoint>]
let main argv =
if argv.Length <> 1 then
failwith "Usage: program <user_id>"
let result =
tryParse argv.[0]
|> Option.bind getCustomerById
|> Option.bind getEmailForCustomer
|> Option.map sendPromotionalEmail
match result with
| Some _ -> printfn "Email sent successfully"
| None -> printfn "No email sent"
0
Note that even though F# is a statically typed language it is not required to explicitly define types for all input parameters and return values, most of the time the compiler can resolve the types so you don’t have to. In the code above I have however defined the types anyway to make it clearer how to work with Option, bind and map.
As you can see there are three functions that returns Options, tryParse that returns Option<CustomerId>, getCustomerById that returns Option<Customer>, and getEmailForCustomer that returns Option<Email>. Note that even though none of the functions takes an Option as input we are able to pipe the returned value through the functions in the main function using Option.bind and Option.map. Just like we were able to do in C# using language-ext.
Conclusion
In this simple scenario it was quite simple to port the C# implemenation to F#, so much that I would like to explore other scenarios as well and how to implement them using F#.
If using F# is an option it might be worth considering it, and get the full experience, over trying to copy the behavior by using language-ext. I will continue exploring F# and how it compares to C# in different scenarios. I believe it will be an interesting journey.