-
Notifications
You must be signed in to change notification settings - Fork 5
Functional way
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 criticized one, but it's 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
resolves a problem, 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.