Haskell defines IO as an explicit context for handling effects. In F#, you can experiment with using Async as a surrogate.

As a reaction to my article on the relationship between Functional architecture and Ports and Adapters, Szymon Pobiega suggested on Twitter:

random idea: in c# “IO Int” translates to “Task<int>”. By consistently using this we can make type system help you even in c#
If you know Haskell, you might object to that statement, but I don't think Szymon meant it literally. It's an idea worth exploring, I think.

Async as an IO surrogate #

Functions in Haskell are, by default, pure. Whenever you need to do something impure, like querying a database or sending an email, you need to explicitly do this in an impure context: IO. Don't let the name IO mislead you: it's not only for input and output, but for all impure actions. As an example, random number generation is impure as well, because a function that returns a new value every time isn't pure. (There are ways to model random number generation with pure functions, but let's forget about that for now.)

F#, on the other hand, doesn't make the distinction between pure and impure functions in its type system, so nothing 'translates' from IO in Haskell into an equivalent type in F#. Still, it's worth looking into Szymon's suggestion.

In 2013, C# finally caught up with F#'s support for asynchronous work-flows, and since then, many IO-bound .NET APIs have gotten asynchronous versions. The underlying API for C#'s async/await support is the Task Parallel Library, which isn't particularly constrained to IO-bound operations. The use of async/await, however, makes most sense for IO-bound operations. It improves resource utilisation because it can free up threads while waiting for IO-bound operations to complete.

It seems to be a frequent observation that once you start using asynchronous operations, they tend to be 'infectious'. A common rule of thumb is async all the way. You can easily call synchronous methods from asynchronous methods, but it's harder the other way around.

This is similar to the distinction between pure and impure functions. You can call pure functions from impure functions, but not the other way around. If you call an impure function from a 'pure' function in F#, the caller also automatically becomes impure. In Haskell, it's not even possible; you can call a pure function from an IO context, but not the other way around.

In F#, Async<'a> is more idiomatic than using Task<T>. While Async<'a> doesn't translate directly to Haskell's IO either, it's worth experimenting with the analogy.

Async I/O #

In the previous Haskell example, communication with the database was implemented by functions returning IO:

getReservedSeatsFromDB :: ConnectionString -> ZonedTime -> IO Int

saveReservation :: ConnectionString -> Reservation -> IO ()

By applying the analogy of IO to Async<'a>, you can implement your F# data access module to return Async work-flows:

module SqlGateway =
    // string -> DateTimeOffset -> Async<int>
    let getReservedSeats connectionString (date : DateTimeOffset) =
        // ...
 
    // string -> Reservation -> Async<unit>
    let saveReservation connectionString (reservation : Reservation) =
        // ...

This makes it harder to compose the desired imp function, because you need to deal with both asynchronous work-flows and the Either monad. In Haskell, the building blocks are already there in the shape of the EitherT monad transformer. F# doesn't have monad transformers, but in the spirit of the previous article, you can define a computation expression that combines Either and Async:

type AsyncEitherBuilder () =
    // Async<Result<'a,'c>> * ('a -> Async<Result<'b,'c>>)
    // -> Async<Result<'b,'c>>
    member this.Bind(x, f) =
        async {
            let! x' = x
            match x' with
            | Success s -> return! f s
            | Failure f -> return Failure f }
    // 'a -> 'a
    member this.ReturnFrom x = x
 
let asyncEither = AsyncEitherBuilder ()

This is the minimal implementation required for the following composition. A more complete implementation would also define a Return method, as well as other useful methods.

In the Haskell example, you saw how I had to use hoistEither and liftIO to 'extract' the values within the do block. This is necessary because sometimes you need to pull a value out of an Either value, and sometimes you need to get the value inside an IO context.

In an asyncEither expression, you need similar functions:

// Async<'a> -> Async<Result<'a,'b>>
let liftAsync x = async {
    let! x' = x
    return Success x' }
 
// 'a -> Async<'a>
let asyncReturn x = async { return x }

I chose to name the first one liftAsync as a counterpart to Haskell's liftIO, since I'm using Async<'a> as a surrogate for IO. The other function I named asyncReturn because it returns a pure value wrapped in an asynchronous work-flow.

You can now compose the desired imp function from the above building blocks:

let imp candidate = asyncEither {
    let! r = Validate.reservation candidate |> asyncReturn
    let! i =
        SqlGateway.getReservedSeats connectionString r.Date
        |> liftAsync
    let! r = Capacity.check 10 i r |> asyncReturn
    return! SqlGateway.saveReservation connectionString r
            |> liftAsync }

Notice how, in contrast with the previous example, this expression uses let! and return! throughout, but that you have to compose each line with asyncReturn or liftAsync in order to pull out the appropriate values.

As an example, the original, unmodified Validate.reservation function has the type ReservationRendition -> Result<Reservation, Error>. Inside an asyncEither expression, however, all let!-bound values must be of the type Async<Result<'a, 'b>>, so you need to wrap the return value Result<Reservation, Error> in Async. That's what asyncReturn does. Since this expression is let!-bound, the r value has the type Reservation. It's 'double-unwrapped', if you will.

Likewise, SqlGateway.getReservedSeats returns a value of the type Async<int>, so you need to pipe it into liftAsync in order to turn it into an Async<Result<int, Error>>. When that value is let!-bound, then, i has the type int.

There's one change compared to the previous examples: the type of imp changed. It's now ReservationRendition -> Async<Result<unit, Error>>. While you could write an adapter function that executes this function synchronously, it's better to recall the mantra: async all the way!

Async Controller #

Instead of coercing the imp function to be synchronous, you can change the ReservationsController that uses it to be asynchronous as well:

// ReservationRendition -> Task<IHttpActionResult>
member this.Post(rendition : ReservationRendition) =
    async {
        let! res = imp rendition
        match res with
        | Failure (ValidationError msg) ->
            return this.BadRequest msg
        | Failure CapacityExceeded ->
            return this.StatusCode HttpStatusCode.Forbidden
        | Success () -> return this.Ok () }
    |> Async.StartAsTask

Notice that the method body uses an async computation expression, instead of the new asyncEither. This is because, when returning a result, you finally need to explicitly deal with the error cases as well as the success case. Using a standard async expression enables you to let!-bind res to the result of invoking the asynchronous imp function, and pattern match against it.

Since ASP.NET Web API support asynchronous controllers, this works when you convert the async work-flow to a Task with Async.StartAsTask. Async all the way.

In order to get this to work, I had to overload the BadRequest, StatusCode, and Ok methods, because they are protected, and you can't use protected methods from within a closure. Here's the entire code for the Controller, including the above Post method for completeness sake:

type ReservationsController(imp) =
    inherit ApiController()
 
    member private this.BadRequest (msg : string) =
        base.BadRequest msg :> IHttpActionResult
    member private this.StatusCode statusCode =
        base.StatusCode statusCode :> IHttpActionResult
    member private this.Ok () = base.Ok () :> IHttpActionResult
 
    // ReservationRendition -> Task<IHttpActionResult>
    member this.Post(rendition : ReservationRendition) =
        async {
            let! res = imp rendition
            match res with
            | Failure (ValidationError msg) ->
                return this.BadRequest msg
            | Failure CapacityExceeded ->
                return this.StatusCode HttpStatusCode.Forbidden
            | Success () -> return this.Ok () }
        |> Async.StartAsTask

As you can see, apart from the overloaded methods, the Post method is all there is. It uses the injected imp function to perform the actual work, asynchronously translates the result into a proper HTTP response, and returns it as a Task<IHttpActionResult>.

Summary #

The initial idea was to experiment with using Async as a surrogate for IO. If you want to derive any value from this convention, you'll need to be disciplined and make all impure functions return Async<'a> - even those not IO-bound, but impure for other reasons (like random number generators).

I'm not sure I'm entirely convinced that this would be a useful strategy to adopt, but on the other hand, I'm not deterred either. At least, the outcome of this experiment demonstrates that you can combine asynchronous work-flows with the Either monad, and that the code is still manageable, with good separation of concerns. Since it is a good idea to access IO-bound resources using asynchronous work-flows, it's nice to know that they can be combined with sane error handling.


Comments

In my opinion, the true purpose of IO is to allow you to write pure effectful functions. For example, Haskell's putStrLn is referentially transparent - i.e., [putStrLn "hello", putStrLn "hello"] has the same meaning as replicate 2 $ putStrLn "hello". This means you can treat a function that returns an IO the same way you'd treat any other function.

This is possible for two reasons: 1) IO actions are not run until they hit the "edge of the universe" where unsafePerformIO is called and 2) they don't memoize the result - if an IO action is executed twice, it will calculate two possibly distinct values.

The problem with Task is that it's not referentially transparent.

new[] { Task.Run(() => WriteLine("hello")), Task.Run(() => WriteLine("hello")) } is not the same as var t = Task.Run(() => WriteLine("hello")) + new [] { t, t }. Even if you use new Task() instead of Task.Run to delay its execution, you still wouldn't be able to factor out the task because it memoizes the result (in this case, void), so you'd still see "hello" only once on the screen.

Since Task cannot be used to model pure functions, I don't think it's a suitable replacement for IO.

2016-04-16 15:55 UTC

Diogo, thank you for writing. The point about Tasks not being referentially transparent is well taken. You are, indeed, correct.

F# asynchronous work-flows work differently, though. Extending the experiment outlined in this article, you can attempt to 'translate' putStrLn to F#. Since the return type of putStrLn is IO (), you'll need a function that returns Async<unit>. Here's my naive attempt:

let aprintfn fmt = async { return printfn fmt }

Corresponding to your two Haskell examples, consider, then, these two F# values:

let explicitSeq = seq { yield aprintfn "hello"yield aprintfn "hello" }
let replicatedSeq = Seq.replicate 2 <| aprintfn "hello"

Both of these values have the type seq<Async<unit>>, and I'd expect them to be equivalent.

In Haskell, I used sequence to run your code in GHCI, turning [IO ()] into IO [()]. In F#, you can do something similar:

// seq<Async<'a>> -> Async<seq<'a>>
let asequence xs = async { return Seq.map Async.RunSynchronously xs }

Using this function, you can evaluate both expressions:

> asequence explicitSeq |> Async.RunSynchronously;;
hello
hello
val it : seq<unit> = seq [null; null]
> asequence replicatedSeq |> Async.RunSynchronously;;
hello
hello
val it : seq<unit> = seq [null; null]

As you can see, the result is the same in both cases, like you'd expect them to be.

I'm still not claiming that Async<'a> is a direct translation of Haskell's IO, but there are similarities. Admittedly, I've never considered whether F# asynchronous work-flows satisfy the monad laws, but it wouldn't surprise me if they did.

2016-04-20 14:55 UTC

Ah, I see! It wasn't clear to me that async workflows were referentially transparent. That being the case, it seems like they are, indeed, a good substitute for IO a.

It seems like the F# team got async right :) The C# team should take a page from their book...

2016-05-04 23:05 UTC

I know what functional programmers generally and Mark specially mean when they say that function is (or is not) referentially transparent. Namely, a function is referentially transparent if and only if its behavior is unchanged when memozied.

I see how caching behavior varies among Haskell's IO, F#'s Async, and C#'s Task. However, the previous comments are the only times that I have anyone say that a type is (or is not) referentially transparent. In general, what does it mean for a type to be (or not to be) referentially transparent?

2020-08-02 15:02 UTC

Tyson, language is imprecise. I take it that we meant that the behaviour that the type (here: an object) affords helps preserve referential transparency. It's fairly clear to me that it makes sense to say that Task<T> isn't referentially transparent, because of the behaviour shown by the above examples.

In C# or F#, we can't universally state that Async is always referentially transparent, because these languages allow impure actions to be passed to the type's members. Still, if we pass pure functions, the members are referentially transparent too.

In object-oriented programming, we often think of a type as an object: data with behaviour. In Haskell, you often have a type and an associated type class. In F#, some containers (like Async) have an associated computation expression builder; again, that's a set of functions.

If a set of associated functions or members are all referentially transparent, it makes sense to me to talk about the type as being referentially transparent, but all this is a view I've just retconned as I'm writing. You may be able to poke holes in it, so don't take it for more than it is.

2020-08-06 8:48 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 11 April 2016 12:38:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 11 April 2016 12:38:00 UTC