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, but the ugly part hidden. What about running more than one query on one open connection? Code like this:

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

allows it, 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 queryfunction 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