4
votes

I would like to define a default value for a discriminated union, like this:

open System.Linq

type Result =
| Ok
| Error

let results : seq<Result> = [] |> Seq.ofList

let firstResult = results.FirstOrDefault()
// I want firstResult to be Error, currently it is null.

option<'a> works in this way (firstResult would be None), so it should be possible. Thanks for your help.

Edit: I'm using SQLDataProvider and would like to write code like

let userEmail =
    query {
        for user in dbContext.Public.Users do
        where (user.Id = 42)
        select (Ok user.Email)
        headOrDefault
        // should result in Error
        // when no user with Id=42 exists 
    }

My actual result type looks like this:

type Result<'a> =
| Ok of 'a
| Failure of string // Expected, e. g. trying to log in with a wrong password
| Error // Unexpected

Returning an option, the caller would not be able to differentiate between failures and errors.

3
There is actually a built in Result<'a> type in F# 4.1. You are not getting the default value for option<'a>, but the value for an empty list, which is null, which shows up as None in that case. Basically Error is not a default value, it's just a constructor for the Error case. To get it you would need to pattern match or use the approach in the answer. Also, using Linq in F# is possible but not idiomatic. - s952163
Thanks for the clarification. You could just use exception and try/with to handle them, especially since many will occur on the I/O side, and ROP which is great for DDD is not so great there. The option type then should handle the case when no user id exists, then pattern match on None. - s952163

3 Answers

7
votes

In general F# avoids the concept of defaults, instead making everything as explicit as possible. It's more idiomatic to return an option and for the caller to decide what to default to for a particular use case.

The FirstOrDefault method can only return .NET's default value for any type. So for any class it would return null, and for a number it would return zero.

I would recommend this approach instead assuming your desired default is Ok:

results |> Seq.tryHead |> Option.defaultValue Ok
4
votes

You might be able to do this using the UseNullAsTrueValue compilation parameter. This tells the compiler to use null as an internal representation of one of the parameter-less cases. It is a bit of a hack (because this is meant mostly for performance optimization and here we are misusing it somewhat, but it might work).

I don't have SQL database setup to try that with the SQL provider, but the following works in memory and I think it should work with SQL too:

[<CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueValue)>]
type Result<'a> =
  | Ok of 'a
  | Failure of string
  | Error

let userEmail =
  query {
    for a in [1] do
    where (a = 2)
    select (Ok a)
    headOrDefault }

If you run this, F# interactive prints that userEmail = null, but that's fine and the following is true:

userEmail = Error
2
votes

If you want to represent issues with the data or the query parameters, such as not finding a particular record, as being distinct from other types of failures, such as not connecting to the database or hitting an unhandled exception, you can create a discriminated union to represent your expected data/query problems explicitly. You can then return that DU instead of string as the data for the Error case, and you won't really need the Failure case. Consider something like this:

type SqlError =
| NoMatchingRecordsFound of SqlParameter list
| CouldNotConnectToDatabase
| UserDoesNotHaveQueryPermissions
| UnhandledException of exn

I would suggest taking a look at Railway-Oriented Programming and following the pattern of defining a discriminated union for your different error cases. You can still include an error message in that DU, but I would suggest using explicit cases for all your different expected failures, and then having an "UnhandledException" or similar case for your unexpected errors, perhaps with an exn for the data.

If you're interested, I have a library on GitHub/NuGet that puts all the ROP stuff together and adds interoperability with the Task, Async, and Lazy types in one computation builder, so you don't have to include all that code in your own project.

EDIT

Here's a complete example of doing it ROP-style (using the linked framework):

open FSharp.Control
open System.Linq

// Simulate user table from type-provider
[<AllowNullLiteral>]
type User() =
    member val Id = 0 with get,set
    member val Name = "" with get,set

// Simulate a SQL DB
let users = [User(Id = 42, Name = "The User")].AsQueryable()

// Our possible events from the SQL query
type SqlEvent =
| FoundUser
| UserIdDoesNotExist of int
| CouldNotConnectToDatabase
| UnhandledException of exn

// Railway-Oriented function to find the user by id or return the correct error event(s)
let getUserById id =
    operation {
        let user =
            query {
                for user in users do
                where (user.Id = id)
                select user
                headOrDefault
            }

        return!
            if user |> isNull
            then Result.failure [UserIdDoesNotExist id]
            else Result.successWithEvents user [FoundUser]
    }

Caling getUserById 42 would return a successfully completed operation with the user and the event FoundUser. Calling getUserById for any other number (e.g. 0) would return a failed operation with the error event UserIdDoesNotExist 0. You would add more events as necessary.