F# If statement type mismatch

Viewed 135

i am in a bit of a pickle, i have tried for 2 hours straight trying to get this code to work and im lost.

let DropColumn list =
    if List.exists List.isEmpty list then "empty value"
    else
    list |> List.map List.tail

This gives me an error error FS0001: The type 'string' does not match the type ''a list'

2 Answers

The usual way to deal with failure in functional programming is to use the Result type, which is essentially defined as:

type Result<'T,'TError> =
    | Ok of 'T
    | Error of 'TError

To apply it to your code we can just wrap the string in the unhappy path with Error and the list from the happy path with Ok:

let DropColumn list =
    if List.exists List.isEmpty list then
        Error "empty value"
    else
        list |> List.map List.tail |> Ok

Then to use it you can use pattern matching:

match DropColumn myList with
| Ok newList ->
    newList
| Error message ->
    printfn "Error occurred: %s" message
    []

The answer given by @glennsl is correct and in many cases the preferred way. However, I'd like to add that there are two other common ways of dealing with invalid input:

  • Raise an exception. Use this for exceptional cases only, i.e. where you expect your code to halt as the result of invalid data. Do not use it for normal validation where you expect that data can often be wrong.
  • Use option. This is similar to using Result, but doesn't maintain information for the invalid case. This approach is very common and used a lot in library functions like List.tryFind, List.tryHead etc.

Raise an exception

In the comments you show you already know this option exists, but let's give it here for completeness:

let dropColumnOrRaise list =
    if List.exists List.isEmpty list then failwith "empty value"
    else
    list |> List.map List.tail

Use option

This method usually requires that the business logic that shows an error or does recovery, goes elsewhere.

let tryDropColumn list =
    if List.exists List.isEmpty list then None
    else
    list 
    |> List.map List.tail
    |> Some

Use it as follows:

match tryDropColumn myCols with
| Some columns -> 
     // do something with valid columns, i.e., display them
     printfn "%i columns remaining (List.length (List.head myCols))"
| None -> 
     // error recovery or showing a message
     printfn "No column selected"

When you are dealing with several functions that operate on data that all return option, you can pipe them together with Option.bind (or Option.map if a function doesn't return option).

myCols
|> tryDropColumn
|> Option.map logColumns      // function that always succeeds
|> Option.bind tryAtLeastTwoColumns  // function that returns None on 1 or 0
|> Option.map showColumns

The code above removes the need to have a match x with for each returned option. Similar code can be used for Result from the previous answer.

Related