+ All Categories
Home > Education > Understanding F# Workflows

Understanding F# Workflows

Date post: 10-May-2015
Category:
Upload: mdlm
View: 5,240 times
Download: 0 times
Share this document with a friend
Description:
Scott Theleman - Understanding F# WorkflowsF# Workflows are a powerful and elegant tool for solving many real-world problems, though they can be rather daunting at first. We'll survey some ways in which Workflows in the standard F# libraries are used for common development tasks, then dig into detail on how they work. We'll then build a workflow that provides a validation framework that can be used for parsing or other tasks.Scott Theleman is a Software Developer with over 10 years professional design and development experience in both small startup and mid-sized corporate/Enterprise environments on applications ranging from desktop GUIs to website/web applications to server side and middleware work. He has also been Technical Lead on several government contracts.He has worked on a diverse range of projects including a network discovery and topology product, a Learning Management System, Enterprise Service Oriented Architecture components for a large and complex search service, and atmospheric and weather sciences applications.Language experience includes C++, Perl, Java and C#. He is currently working as a consultant on various projects including an atmospheric sciences product which will make extensive use of the latest Microsoft technologies including F# and WPF.
Popular Tags:
29
Understanding F# Workflows New England F# User’s Group Presentation (fsug.org) August 2, 2010 Scott Theleman
Transcript
Page 1: Understanding F# Workflows

Understanding F# Workflows

New England F# User’s Group Presentation (fsug.org)August 2, 2010Scott Theleman

Page 2: Understanding F# Workflows

Overview

F# Workflows – closely related to Monads in Haskell and other languages – are a powerful and elegant tool for solving many real-world problems, though they can be rather daunting at first.

We'll survey some ways in which Workflows in the standard F# libraries are used for common development tasks, then dig into detail on how they work.

Finally we’ll build a workflow that provides a validation framework that can be used for parsing or other tasks.

Page 3: Understanding F# Workflows

Intro to Monads

A “Monad” is generally a set of interrelated constructs. At the least, it usually consists of:

A “Monadic Type”

A bind function (sometimes the (>>=) operator is used)

A return function

“When the designers of F# talked with the designers of Haskell about this, they agreed that the word monad is obscure and sounds a little daunting and that using other names might be wise”

— Expert F# 2.0 (Don Syme, Adam Granicz, Antonio Cisternino)

Page 4: Understanding F# Workflows

Characteristics of Monads The Monadic Type “wraps” an underlying type. The monadic

type may be more like an object (which may contain other data or state), or more like a computation or potential computation.

The Return function wraps an underlying type in a monadic type.

The Bind function takes an underlying type as well as a function which maps from the underlying type to a new monadic type, and returns a new monadic type.

By performing this wrapping of underlying types inside a monadic type and providing bind and return, you can now combine computations of that inner type in ways that are difficult or impossible when just dealing with the underlying types.

Page 5: Understanding F# Workflows

Monad StructureMonadic Type

• type M<'T>

• “Wraps” an underlying type

• May be more like an object which may contain other data or state (e.g. M<int>, M<string>, M<Customer>) or more like a computation or potential computation M<fun () -> Customer>).

Return Function

• let return (v : 'T) = M(v)

• “Wraps” an underlying type in the Monadic type

Bind Function

• let bind (v : M<'T>) (f : 'T -> M<'U>) : M<'U> = f v

• Takes an underlying type as well as a function which maps from the underlying type to a new monadic type, and returns a new monadic type

• By performing this wrapping of underlying types inside a monadic type and providing bind and return, you can now combine computations of that inner type in ways that are difficult or impossible when just dealing with the underlying types.

Page 6: Understanding F# Workflows

Uses of Monads aka WorkflowsUses F# Examples

Performing optional computations, which short-circuit if any part fails

attempt, maybe

Same as above, plus diagnostic information on success and/or failure

parse

Performing sequences of operations, then collecting the results

seq

Threading accumulated state throughout a process, without having to explicitly handle the state at each step in the process

processor, state, validate

Performing different parts of the workflow in parallel, on separate threads, machines, etc.

async

Performing side-effects (display, logging, etc.), without explicitly coding for this within the workflow

logger, Haskell IO

Transforming pieces of the workflow to process in a different way – e.g. LINQ to SQL or running on a GPU

query

Page 7: Understanding F# Workflows

One use of Monads: Sequential Workflows

As noted, there are many uses and varieties of Monads

We will concentrate on solving a typical sequential workflow style problem

First showing other ways this has been done without workflows, then building up to using an F# workflow

Page 8: Understanding F# Workflows

Sequential Workflows: If/else The following code takes an initial input (of type T) and

performs 3 sets of transformations on it, each time returning a tuple of bool and Result object (of type T). If there is a failure at any step, the entire operation is short circuited.

let process1 = true, input // do something with inputlet process2 = false, inputlet process3 = true, input let Execute (input : 'T) = let ok, result1 = process1 input if ok then let ok, result2 = process2 result1 if ok then let ok, result3 = process3 result2 ok, result3 else false, result2 else false, result1

Page 9: Understanding F# Workflows

If/else: Problems

The processX() methods and their callers all must know about the input and result types. Generics help the situation, but still these methods are hard-wired for those specific types, plus the success/failure Boolean.

Also, the 'T in Execute() and processX() is always the same!

It’s getting pretty messy, and we’ve only done 3 transformations. Pretty soon the code is going to be off the right side of the screen!

We have to explicitly handle failure at every step of the process

Lots of redundancy. We said “ok” 6 times!

We don’t have any information about what went wrong. Though we could define some sort of error type (see next example…).

Page 10: Understanding F# Workflows

Sequential Workflows: Option and match The following code tries to improve on the last sample. It now includes a

Result<'T> type which we could expand upon to return detailed error information. It also uses pattern matching, which makes the code a bit clearer.

type Result<'T> = | Success of 'T | Failure of string let process1 input = Success(input) // do something interesting herelet process2 input = Failure("Some error")let process3 input = Success(input)  let Process (input : 'T) = let res1 = process1 input match res1 with | Failure _ -> res1 | Success v -> let res2 = process2 v match res2 with | Failure _ -> res2 | Success v -> let res3 = process3 v res3

Page 11: Understanding F# Workflows

Option/match: Problems

Better than if/else, but…

Still messy and redundant and again the code is drifting off the right side of the screen

The processX() methods and their callers still must all know about the input and result types. The 'T in Execute() and processX() is still always the same

We still have to explicitly handle failure at every step of the process

The Result<'T> type does seem like a nice idea

Page 12: Understanding F# Workflows

Sequential Workflows: try/catch Try/catch could simplify/aggregate and improve things a bit – though just for this

particular case. It does look nice and streamlined, which is one thing we are looking for.

exception MyProcessException of string let process1 input = inputlet process2 input = raise <| MyProcessException("An error occurred“)let process3 input = input // processX now accept and return T// No Result any more; exceptions are used insteadlet Execute (input : 'T) = try let v1 = process1 input let v2 = process2 v1 let v3 = process3 v2 v3 with | :? MyProcessException as ex -> // Catch application-specific error...do we throw or return a Result?? reraise () | exn -> // A "real" exception...what to do here? reraise () let Caller<'T> v = // This will throw underlying exception on failure // Caller's caller will also have to handle it Execute v

Page 13: Understanding F# Workflows

try/catch: Problems

Getting better, but…

Now we’re using the try/catch exception mechanism for handling short-circuiting errors rather than real exception cases. Is the exception just due to a typical error in processing or is it a “real” exception?

What does the caller do in this case? Note also that it becomes difficult for the caller to now be part of a larger workflow, or else a lot of hard-coded wireup

The “inner workflows” called by the top-level workflow all need to have try/catch and also throw the same Exception type (e.g. MyProcessException).

Page 14: Understanding F# Workflows

Sequential Workflows: Extension Methods Using Extension Methods to “chain” or “pipeline” (in a C#/Java kind of way). The output of one function feeds the input of the next. Then, we wrap the whole thing in a try/catch.

exception MyException of string

type WrapperObject(v : 'T) =    let value = v    member x.Value with get() = v

module WrapperObjectExtensions =    type WrapperObject with        member x.Process1() = let v = x.Value + " Process1" in WrapperObject(v)        member x.Process2() = let v = x.Value + " Process2" in WrapperObject(v)         member x.Process3() = let v = x.Value + " Process3" in WrapperObject(v) open WrapperObjectExtensions

let Execute (input : string) =    let wrapper = WrapperObject(input)

    try        let res =  wrapper.Process1().Process2().Process3()        res.Value    with    | :? MyException as ex ->        // throw or return a Result?        reraise ()    | exn ->        // A "real" exception        // What to do here?        reraise ()

Page 15: Understanding F# Workflows

Sequential Workflows: Chained Objects Using Interfaces, we return instances of object, on which further Process()

can be called.

module ChainableObjectsWorkflow

exception MyException of string

type IChainableObject<'T> =    abstract Value : unit -> 'T with get    abstract Process : ('T -> 'T) -> IChainableObject<'T>

type ChainableObject<'T>(v : 'T) as this =    let value = v

    interface IChainableObject<'T> with        member x.Value with get() = value        override x.Process (f : ('T -> 'T)) =            let v = (this :> IChainableObject<_>).Value            let res = f v            ChainableObject(res) :> IChainableObject<'T>

let process1 (s : string) = s + " Process1 applied"let process2 (s : string) = raise <| MyException("Error")let process3 (s : string) = s + " Process3 applied"

Page 16: Understanding F# Workflows

Sequential Workflows: Chained Objects (continued) Execute() function

let Execute (input : string) =    let co = ChainableObject(input) :> IChainableObject<_>

    try        let res = co.Process(process1).Process(process2).Process(process3)        res.Value    with    | :? MyException as ex ->        // throw or return a Result?        reraise ()    | exn ->        // A "real" exception        // What to do here?        reraise ()

Page 17: Understanding F# Workflows

Sequential Workflows: Pipelining Similar to Extension Methods but with more idiomatic F# syntax with (|>)

instead of dot syntax

exception MyException of string let process1 input = inputlet process2 input = raise <| MyException("An error occurred")let process3 input = input let Execute (input : 'T) = try input |> process1 |> process2 |> process3 with | :? MyException as ex -> // throw or return a Result? reraise () | exn -> // A "real" exception // What to do here? reraise ()

Page 18: Understanding F# Workflows

Chaining, Pipelining, etc.: Problems

Getting better, but…

Still using the try/catch exception mechanism for handling short-circuiting errors rather than real exception cases.

We just get the result of the overall computation, but not each individual piece. What if the workflow wants to perform additional processing on pieces?

Once again, the 'T in Execute() and processX() is always the same

Page 19: Understanding F# Workflows

Help from Continuations

module Continuationstype Result<'T> = | Success of 'T | Failure of string

let process1 = (fun v -> Success("Process 1: " + v))let process2 = (fun v -> Failure("Process 2: An error occurred"))let process3 = (fun v -> Success("Process 3: " + v))

// Run f on v. If is succeeds, then call cont on that result, else return Failure// Note that cont can transform the result into another typelet executeCont v (f : 'a -> Result<'a>) (cont : 'a -> Result<'b>) : Result<'b> = let maybe = f v match maybe with | Failure(err) -> Failure(err) | Success(result) -> cont result

let Execute v : Result<_> =    executeCont v process1 (fun result1 ->        executeCont result1 process2 (fun result2 ->            executeCont result2 process3 (fun result3 -> Success(result3))))

Page 20: Understanding F# Workflows

Continuations

Now we’re getting somewhere!

Conditional computation – executeCont() can short-circuit

We have access to intermediate results and could use these at any future point in the workflow

The continuation function can transform the type from 'a to 'b. Now the types can be transformed in each stage of the workflow. More generic workflow helper functions (processX()) can be built which can manipulate different types.

Still, ugly syntax. Could we improve on this?

Page 21: Understanding F# Workflows

A Better Way: F# Workflows

First define a “Result” type which can be Success or Failure, plus some additional info

Then define the “Monadic” type which wraps a type 'T into a function, which could be conditionally executed to return a Result

Note that Attempt<'T> is basically a continuation. The Workflow Builder we create next contains the logic to run the continuation (the entire rest of the workflow) after running the current step, or else not run additional Attempts if there is a failure, and simply return out of the entire workflow

type Error = { Message : string }

/// A result/// If success, it contains some object, plus a message (perhaps a logging message)/// If failure, it returns an Error object (which could be expanded to be much richer)type Result<'T> =| Success of 'T * string| Failure of Error

type Attempt<'T> = (unit -> Result<'T>)

Page 22: Understanding F# Workflows

F# Workflow Builder: Helper functions

let succeed (x,msg) = (fun () -> Success(x, msg)) : Attempt<'T>let fail err        = (fun () -> Failure(err)) : Attempt<'T>let failmsg msg     = (fun () -> Failure({ Message = msg })) : Attempt<'T>let runAttempt (a : Attempt<'T>) = a()let bind (f : Attempt<'T>) (rest : 'T -> Attempt<'U>) : Attempt<'U> =    match runAttempt f with    | Failure(msg)           -> fail msg    | Success(res, msg) as v -> rest reslet delay f = (fun () -> runAttempt (f()))let getValue (res:Result<'T>) = match res with    | Success(v,s) -> v    | Failure _ -> failwith "Invalid operation"

Function Purpose

succeed Given a value, return a Success Result wrapped in an Attempt<'T>

fail Return an Error Result wrapped in an Attempt<'T>

runAttempt Given an Attempt<'T>, just runs it

bind Runs the given function. If it succeeds, then run the rest of the computation, otherwise return a Failure Result. Also, maps from 'T to 'U

delay Wraps the entire workflow so it can be executed as needed

getValue Helper to get a value out of a successful Result

Page 23: Understanding F# Workflows

F# Workflow Builder: The Workflow Builder Object

type ProcessBuilder() =    member this.Return(x) = succeed x    member this.ReturnFrom(x) = x    member this.Bind(p, rest) = bind p rest    member this.Delay(f) = delay f    member this.Let(p, rest) : Attempt<'T> = rest p

type Processor() =    static member Run workflow =        runAttempt workflow        let processor = new ProcessBuilder()

Uses the helper functions we just defined to create a “builder” class required by F#

Creates “processor” which is an instance of the builder. This is used to wrap all of these workflows using processor { } notation

Another “static class”, Processor, contains additional helper methods (kind of like the Async class)

Page 24: Understanding F# Workflows

Mapping of Workflow ConstructsWorkflow Builder Function Workflow Keyword

Bind let!

Return return

ReturnFrom return!

Let let

Bind (expr, (fun x -> b.Using(x, fun pat-> cexpr)))

use!

For for

Yield/YieldFrom yield, yield!

Page 25: Understanding F# Workflows

F# Workflow: Final Result

type Customer = { Name : string; Birthdate : DateTime;  CreditScore : int; HasCriminalRecord : bool }

let customerWorkflow c = processor {    let! ageTicket = processCustomer1 c    let! creditTicket = processCustomer2 c    let! criminalTicket = processCustomer3 c

    // Process lots more stuff here...note how we can access result of each step

    // If we didn't get to this point, then the entire workflow would have    // returned Result.Failure with the error message where the workflow failed

    // If we got here, then all OK, assemble results and return

    return ((c, [| ageTicket; creditTicket; criminalTicket |]), "Customer passed all checks!")    }

/// If this succeeds, it returns a Result<Customer,int[]>/// else it returns a Failure with an error messagelet results = Processor.Run (customerWorkflow customer)

See code for full example

Page 26: Understanding F# Workflows

F# Workflows: Bind De-Sugared

let customer = { Name = "Jane Doe"; DateTime.Parse("1/1/1960"); CreditScore = 640; HasCriminalRecord = false }

let customerWorkflow c logger = processor {    let! ageResult  = processCustomer1 (c, logger)    let! creditResult  = processCustomer2 (c, logger)     let! criminalResult = processCustomer3 (c, logger) let ageTicket = getValue(ageResult)    let creditTicket  = getValue(creditResult)    let criminalTicket  = getValue(criminalResult)    return ((c, [| ageTicket; creditTicket; criminalTicket |]), "Customer passed all checks!", logger) }

// De-sugars to:let finalResult = processor.Bind(processCustomer1 c, (fun ageResult -> processor.Bind(processCustomer2 c, (fun creditResult -> processor.Bind(processCustomer3 c, (fun criminalResult -> processor.Let(getValue(ageResult), (fun ageTicket -> processor.Let(getValue(creditTicket), (fun creditTicket -> processor.Let(getValue(criminalResult), (fun criminalTicket -> processor.Return (c, [|ageTicket;creditTicket;criminalTicket|], logger ))))))))))

See code for full example

Page 27: Understanding F# Workflows

ParseWorkflow Example

See example in code

Complete example which parses and validates a fixed-width format specification and returns Line, Position and Message on any errors

Page 28: Understanding F# Workflows

Questions

Questions?

Thank you!

Page 29: Understanding F# Workflows

References

Expert F# 2.0 (Don Syme, et al)

Real World Functional Programming (Tomas Petricek with Jon Skeet) at

http://www.manning.com/petricek/

Lots of F# and Haskell references

Chance Coble “Why use Computation Workflows (aka Monads) in F#?” at

http://leibnizdream.wordpress.com/2008/10/21/why-use-computation-workflo

ws-aka-monads-in-f/

F# Survival Guide: Workflows at:

http://www.ctocorner.com/fsharp/book/ch16.aspx

DevHawk series: http://devhawk.net/CategoryView,category,Monads.aspx

Understanding Haskell Monads (Ertugrul Söylemez) at

http://ertes.de/articles/monads.html

Monads are like Burritos: http://blog.plover.com/prog/burritos.html (and

others)

Many more


Recommended