Skip to content

Functional way

Jacek edited this page Jun 28, 2017 · 12 revisions

Accessing a database is producing side effects. It's unavoidable. But they can be controlled.

In Haskell, every impure function must return IO. Its closest equivalent in F# is Async.

Defining all query functions asynchronous, makes the code more functional:

let getBlog: int -> DataContext -> Blog Async = 
    sql "select id, name, title, description, owner, createdAt, modifiedAt, modifiedBy 
         from Blog 
         where id = @id"

Unfortunately, it's not enough. There is another problem - connection management. Code like this:

async {
    use ctx = createDataContext()
    let! blog = getBlog id ctx
    ...
}

is definitely not functional, since it relies on stateful resource. The solution is to encapsulate connection lifecycle management in some function:

let blog = getBlog id |> run

It's pretty easy to implement - actually run function contains similar code as the criticized one, and the ugly part is hidden.

What about running more than one query on one open connection? Defining some additional function allows it:

(fun ctx ->
    let postId = insertPost post ctx
    insertTags postId tags)
|> run

but doesn't look nice.

The solution lies in category theory. Functions of type DataContext -> 't are examples of Reader monad, and it's possible to define computation expression for composing them, like this:

dbaction {
    let! postId = insertPost id
    do! insertTags postId tags
} |> run

The preferrable way is to use both async and reader:

asyncdb {
    let! postId = insertPost id
    do! insertTags postId tags
} |> runAsync

Why should we care?

It's all about composability. Side effects kill it.

Synchronous query function is completely non-composable.

Asynchronous function with manual connection mamagement is composable only within use statement.

When reducing computation to DataContext -> 't, we achieve full composability.

Clone this wiki locally