We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to publish this job!

Login or register
to save this job!

Login or register
to save interesting jobs!

Login or register
to get access to all your job applications!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Login or register
to save articles!

Login to see the application

Engineers who find a new job through Functional Works average a 15% increase in salary 🚀

You will be redirected back to this page right after signin

Blog hero image

In a Bind with F#

Patrick Tone 31 January, 2022 | 3 min read

While going through the F# exercism track, I stumbled upon F#'s bind function.

One of my favorite things about Exercism is the ability to view other user's published solutions. In this case, the most starred solution for Phone Number by alex123098 had an interesting solution using Result.bind and Result.map (check it out here). Thus begins our journey.

Note: For a more in-depth post, please check out Scott Wlaschin's post on Understanding bind.

Post-Note: srpeterson shared a more advanced solution that uses domain modeling for additional type safety. You can view it here.

"Unbound" Solution

My original solution wasn't the most functional or the easiest to reason through. I really disliked it. I really wanted a "railway oriented programming" way to attack this problem.

module PhoneNumber

open System

let clean (input: string) =
module PhoneNumber

open System

let clean (input: string) =
    let validPunctuation = [ '-'; '('; ')'; '.' ]

    let isInvalidPunctuation c =
        Char.IsPunctuation c
        && not (List.contains c validPunctuation)

    match input with
    | phone when phone |> String.exists Char.IsLetter -> Error "letters not permitted"
    | phone when phone |> String.exists isInvalidPunctuation -> Error "punctuations not permitted"
    | phone ->
        let removeCountryCode (phone: string) =
            if phone.Length = 11 then phone.[1..] else phone

        let filtered = phone |> String.filter Char.IsDigit
        let noCountryCode = filtered |> removeCountryCode

        match noCountryCode with
        | _ when filtered.Length = 11
                 && not (filtered.StartsWith('1')) -> Error "11 digits must start with 1"
        | _ when filtered.Length > 11 -> Error "more than 11 digits"
        | phone when phone.Length <> 10 -> Error "incorrect number of digits"
        | phone when phone.StartsWith('0') -> Error "area code cannot start with zero"
        | phone when phone.StartsWith('1') -> Error "area code cannot start with one"
        | phone when phone.[3] = '0' -> Error "exchange code cannot start with zero"
        | phone when phone.[3] = '1' -> Error "exchange code cannot start with one"
        | phone -> (uint64 >> Ok) phone

So with this "good enough" solution to parse a phone number in mind, let's look at what bind gives us.

Binding the Solution

module PhoneNumber

open System

let private scrub (input: string) =
    let validPunctuation = [ '('; ')'; '-'; '.'; '+'; ' ' ]

    input
    |> String.filter (fun c -> not (List.contains c validPunctuation))

let private validateLength input =
    match String.length input with
    | length when length < 10 -> Error "incorrect number of digits"
    | length when length > 11 -> Error "more than 11 digits"
    | _ -> Ok input

let private validateCountryCode input =
    match String.length input with
    | 11 when input.[0] <> '1' -> Error "11 digits must start with 1"
    | 11 -> Ok input.[1..]
    | _ -> Ok input

let private validateNoLetters input =
    if String.exists Char.IsLetter input then Error "letters not permitted" else Ok input

let private validateNoPunctuation input =
    if String.exists Char.IsPunctuation input then Error "punctuations not permitted" else Ok input

let private validateAreaCode (input: string) =
    match input.[0] with
    | '0' -> Error "area code cannot start with zero"
    | '1' -> Error "area code cannot start with one"
    | _ -> Ok input

let private validateExchangeCode (input: string) =
    match input.[3] with
    | '0' -> Error "exchange code cannot start with zero"
    | '1' -> Error "exchange code cannot start with one"
    | _ -> Ok input

let clean (input: string): Result<uint64, string> =
    input
    |> scrub
    |> validateLength
    |> Result.bind validateCountryCode
    |> Result.bind validateNoLetters
    |> Result.bind validateNoPunctuation
    |> Result.bind validateAreaCode
    |> Result.bind validateExchangeCode
    |> Result.map uint64

Now we can read the clean function and understand what it does at each step. Let's take a closer look at the the signature of validateLength and validateCountryCode.

validateLength string -> Result<string, string>
validateCountryCode string -> Result<string, string>

Notice how validateLength returns Result<string, string>, yet it pipes into validateCountryCode which accepts a string.

🤯

So Result.bind effectively allows us to avoid having to write functions like

let divide (result: Result<int * int, string>): Result<int, string> =
    match result with
    | Ok (_, 0) -> Error "divisor cannot be zero"
    | Ok (dividend, divisor) -> Ok (dividend / divisor)
    | Error e -> Error e

let add1 (result: Result<int, string>): Result<int, string> =
  match result with
  | Ok number -> Ok (number + 1)
  | Error e -> Error e

Ok (10, 0) |> divide |> add1
// Error "divisor cannot be zero"

and instead write

let divide (numbers: int * int): Result<int, string> =
  match numbers with
  | (_, 0) -> Error "divisor cannot be zero"
  | (dividend, divisor) -> Ok (dividend / divisor)

let add1 (number: int): Result<int, string> =
  Ok (number + 1)

Ok (10, 0) |> Result.bind divide |> Result.bind add1
// Error "divisor cannot be zero"

While this is a simple example, hopefully it demonstrates how bind helps us simplify the internals of our functions without losing context.

Originally published on patrickt.one

Author's avatar
Patrick Tone
Software Developer & Architect | Diving into Machine Learning & AI | Building Custom Web Solutions to Accelerate Business

Related Issues

open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Started
  • 0
  • 16
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Started
  • 0
  • 5
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Started
  • 0
  • 5
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Started
  • 0
  • 7
  • Intermediate
  • HTML

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through Functional Works average a 15% increase in salary.

Start with GitHubStart with Stack OverflowStart with Email