Скачать презентацию F Computation Expressions aka workflows or monads Скачать презентацию F Computation Expressions aka workflows or monads

F# Computation Expressions.pptx

  • Количество слайдов: 57

F#: Computation Expressions aka workflows or monads F#: Computation Expressions aka workflows or monads

Prerequisites • • Sequence, list & array expressions Continuation-passing style Параметрический и ad-hoc полиморфизм Prerequisites • • Sequence, list & array expressions Continuation-passing style Параметрический и ad-hoc полиморфизм OOP в F#

Мы уже знакомы с list, array и sequence expressions. Тем не менее, эти механизмы Мы уже знакомы с list, array и sequence expressions. Тем не менее, эти механизмы — не просто синтаксический сахар, а варианты реализации более общей идеи — computation expressions.

Зачем? • Асинхронное программирование (asynchronous workflows) • Упрощение кода • Возможность контролировать ход вычисления Зачем? • Асинхронное программирование (asynchronous workflows) • Упрощение кода • Возможность контролировать ход вычисления • DSLs и Language-Oriented Programming • Аналоги sequence expressions для других типов данных

Sequence comprehensions let rec all. Files. In base. Path = seq { yield! Directory. Sequence comprehensions let rec all. Files. In base. Path = seq { yield! Directory. Get. Files base. Path yield! for subdir in Directory. Get. Directories base. Path do yield! all. Files. In subdir yield! } Seq. comprehensions достаточно мощны, чтобы содержать в себе практически любые конструкции языка, они способны даже использовать рекурсию. Есть только два ограничения: 1. Нельзя использовать mutable, только ref cells 2. Нельзя объявлять новые типы внутри них

Мотивация: безопасное деление type Result = | Success of float | Div. By. Zero Мотивация: безопасное деление type Result = | Success of float | Div. By. Zero let div x y = match y with | 0. -> Div. By. Zero | _ -> Success(x / y)

Посчитаем сопротивление трёх параллельно соединённых резисторов: let resistance r 1 r 2 r 3 Посчитаем сопротивление трёх параллельно соединённых резисторов: let resistance r 1 r 2 r 3 = let step 1 = div 1. r 1 match step 1 with | Div. By. Zero -> Div. By. Zero | Success x -> let step 2 = div 1. r 2 match step 2 with | Div. By. Zero -> Div. By. Zero | Success y -> let step 3 = div 1. r 3 match step 3 with | Div. By. Zero -> Div. By. Zero | Success z -> let final = div 1. (x + y + z) final

Этот код ужасен. Давайте представим, как бы он выглядел в идеале: let resistance r Этот код ужасен. Давайте представим, как бы он выглядел в идеале: let resistance r 1 r 2 r 3 = let_with_check x = div 1. r 1 let_with_check y = div 1. r 2 let_with_check z = div 1. r 3 return_with_check 1. 0 (x + y + z) Но мы не можем создать такую функцию let_with_check — F# не имеет конструкций, позволяющих прервать вычисление на середине (вроде break).

Зато мы можем воспользоваться continuationpassing style: let_with_check result cont = match result with | Зато мы можем воспользоваться continuationpassing style: let_with_check result cont = match result with | Div. By. Zero -> Div. By. Zero | Success x -> cont x cont — это «остаток вычисления» , он будет выполнен только если деление вернуло число.

let resistance' r 1 r 2 r 3 = let_with_check (div 1. r 1) let resistance' r 1 r 2 r 3 = let_with_check (div 1. r 1) (fun x -> let_with_check (div 1. r 2) (fun y -> let_with_check (div 1. r 3) (fun z -> div 1. (x + y + z)))) В другом форматировании: let resistance'' r 1 r 2 r 3 = let_with_check (div 1. r 1) (fun x -> let_with_check (div 1. r 2) (fun y -> let_with_check (div 1. r 3) (fun z -> div 1. (x + y + z))))

На начальном уровне мы можем воспринимать workflows как подобные синтаксические преобразования. Вызов computation expression На начальном уровне мы можем воспринимать workflows как подобные синтаксические преобразования. Вызов computation expression в общей форме выглядит так: comp_expr_builder_instance { //code }

Зададим computation expression builder для нашего примера и создадим его экземпляр: type Safe. Builder Зададим computation expression builder для нашего примера и создадим его экземпляр: type Safe. Builder () = //x : Result //cont : (float -> Result) member this. Bind (x, cont) = match x with | Div. By. Zero -> Div. By. Zero | Success x -> cont x member this. Return (x : 'a) = x let safe = Safe. Builder()

Теперь мы можем легко переписать наше вычисление следующим образом: let resistance. Safe r 1 Теперь мы можем легко переписать наше вычисление следующим образом: let resistance. Safe r 1 r 2 r 3 = safe { let! x = div 1. r 1 let! y = div 1. r 2 let! z = div 1. r 3 return div 1. (x + y + z) }

На самом деле let! и return — это просто синтаксический сахар для вызова методов На самом деле let! и return — это просто синтаксический сахар для вызова методов Bind и Return нашего класса-builder’a. Без «сахара» получим: let tr r 1 r 2 r 3 = safe. Bind( (div 1. r 1), (fun x -> safe. Bind( (div 1. r 2), (fun y -> safe. Bind( (div 1. r 3), (fun z -> safe. Return( div 1. (x + y + z))))

В workflow можно определить методы, которые будут привязаны к другим операторам, вроде yield, yield! В workflow можно определить методы, которые будут привязаны к другим операторам, вроде yield, yield! и return! или даже просто к последовательности операций — «эквивалентно» expr — терминал «cexpr» — нетерминал Полный список в спецификации F#

member Bind: M<'a> * ('a -> M<'b>) -> M<'b> let! pat = expr in member Bind: M<'a> * ('a -> M<'b>) -> M<'b> let! pat = expr in cexpr b. Bind (expr, (fun pat -> «cexpr» )) //Bind: //M * (unit -> M<'b>) -> M<'b> do! expr in cexpr b. Bind (expr, (fun () -> «cexpr» ))

member Using: 'a * ('a -> M<'b>) -> M<'b> when 'a : > System. member Using: 'a * ('a -> M<'b>) -> M<'b> when 'a : > System. IDisposable use pat = expr in cexpr b. Using (expr, (fun pat -> «cexpr» )) use! pat = expr in cexpr b. Bind (expr, (fun x -> b. Using (x, fun pat -> «cexpr» )))

member Return: 'a -> M<'a> return expr b. Return expr member Return. From: 'a member Return: 'a -> M<'a> return expr b. Return expr member Return. From: 'a -> M<'a> return! expr b. Return. From expr

member Yield: 'a -> M<'a> yield expr b. Yield expr member Yield. From: seq<'a> member Yield: 'a -> M<'a> yield expr b. Yield expr member Yield. From: seq<'a> -> M<'a> yield! expr b. Yield. From expr

if expr then cexpr 1 else cexpr 2 if expr then «cexpr 1» else if expr then cexpr 1 else cexpr 2 if expr then «cexpr 1» else «cexpr 2» Но если нет ветви else, то: member Zero: unit -> M if expr then cexpr 1 if expr then «cexpr 1» else b. Zero()

member Combine: M<unit> * M<'a> -> M<'a> member Combine: M<'a> * M<'a> -> M<'a> member Combine: M * M<'a> -> M<'a> member Combine: M<'a> * M<'a> -> M<'a> member Delay: (unit -> M<'a>) -> M<'a> cexpr 1 cexpr 2 b. Combine ( «cexpr 1» , b. Delay(fun () -> «cexpr 2» ))

member While: (unit -> bool) * M<'a> -> M<'a> while expr do cexpr b. member While: (unit -> bool) * M<'a> -> M<'a> while expr do cexpr b. While ((fun () -> expr), b. Delay (fun () -> «cexpr» ))

member For: seq<'a> * ('a -> M<'b>) -> M<'b> for pat in expr do member For: seq<'a> * ('a -> M<'b>) -> M<'b> for pat in expr do cexpr b. For (expr, (fun pat -> «cexpr» ))

member Try. With: M<'a> * (exceptn -> M<'a>) -> M<'a> try expr with patt member Try. With: M<'a> * (exceptn -> M<'a>) -> M<'a> try expr with patt -> cexpr b. Try. With(expr, (fun v -> match v with | (patt: exn) -> «cexpr» | _ -> reraise exn))

member Try. Finally: M<'a> * (unit -> unit) -> M<'a> try expr finally cexpr member Try. Finally: M<'a> * (unit -> unit) -> M<'a> try expr finally cexpr b. Try. Finally(expr, (fun () -> «cexpr» ))

Пример: вычисления с округлением Создадим параметризованное вычислительное выражение для представления вычислений с округлениями до Пример: вычисления с округлением Создадим параметризованное вычислительное выражение для представления вычислений с округлениями до определённого количества цифр: type Rounding. Workflow (sig. Digits : int) = let round (x : float) = Math. Round(x, sig. Digits) member this. Bind(x, cont) = let result = round x cont result member this. Return x = round x

let round sig. Digits = new Rounding. Workflow(sig. Digits) let rounded. Calculation = round let round sig. Digits = new Rounding. Workflow(sig. Digits) let rounded. Calculation = round 4 { let! x = Math. PI / 2. let! y = 0. 5 / 7. return x * y }

Фактически, workflows позволяют нам выполнять некий промежуточный код между шагами вычисления. Фактически, workflows позволяют нам выполнять некий промежуточный код между шагами вычисления.

Сайд-эффекты let rounded. Calculation = round 4 { let! x = Math. PI / Сайд-эффекты let rounded. Calculation = round 4 { let! x = Math. PI / 2. . printfn "%f" z //сайд-эффект return x * y * z } Сайд-эффект произойдёт только при конструировании значения в первый раз. Если мы хотим, чтобы сайд-эффект происходил при каждом вызове rounded. Calculation, мы должны использовать метод Delay().

type Rounding. Workflow' (sig. Digits : int) = let round. . . member this. type Rounding. Workflow' (sig. Digits : int) = let round. . . member this. Bind(x, cont) = let result = round x cont result member this. Return x = round x member this. Delay f = fun x -> this. Bind(this. Return x, f) //или: member this. Delay f = fun () -> f ()

Теперь сайд-эффект будет происходить при каждом вызове значения (которое теперь будет функцией), а не Теперь сайд-эффект будет происходить при каждом вызове значения (которое теперь будет функцией), а не при его создании: let round' sig = new Rounding. Workflow'(sig) let rnded. Calc' = round' 4 { let! x = Math. PI / 2. . . . printfn "%f" z //сайд-эффект не происходит return x * y * z } let x = rnded. Calc' () //сайд-эффект происходит let y = rnded. Calc' () //сайд-эффект происходит

Stack Workflow Мы можем определить выч. выражение для того, чтобы конструировать стеки, аналогично тому, Stack Workflow Мы можем определить выч. выражение для того, чтобы конструировать стеки, аналогично тому, как работают sequence expressions: let s = stack { for x in 0. . 10 -> x yield 12 yield! [50. . 60] try yield 1 / 0 with | _ -> yield 0 }

В подобных «генераторных» выч. выражениях мы можем не определять Bind и Return — они В подобных «генераторных» выч. выражениях мы можем не определять Bind и Return — они не имеют особого смысла. Зато одним из основных методов становятся Yield и Yield. From. Наконец, мы можем использовать ad-hoc полиморфизм: type Stack. Expressions. Builder() = member this. Yield x = new Stack<_>([x]) member this. Yield. From (s : 'a seq) = new Stack<_>(s) //чтобы не нарушить порядок member this. Yield. From (s : Stack<_>) = s

Чтобы иметь возможность записать whileциклы, мы будем производить всё вычисление в конце построения выражения, Чтобы иметь возможность записать whileциклы, мы будем производить всё вычисление в конце построения выражения, использую метод Run: member this. Delay f = f //вычисляем всё в конце member x. Run f = f() Эти методы будут использованы так: let b = builder-expr in b. Run (b. Delay(fun () -> «cexpr» ))

Метод для комбинации двух выражений: member this. Combine(s 1 : Stack<_>, s 2 : Метод для комбинации двух выражений: member this. Combine(s 1 : Stack<_>, s 2 : unit -> Stack<_>) = let s 2' = s 2(). To. Array() |> Array. rev for elem in s 2' do s 1. Push elem s 1 cexpr 2 b. Combine ( «cexpr 1» , b. Delay(fun () -> «cexpr 2» ))

Теперь запишем метод для while: member this. While(p : unit -> bool, body : Теперь запишем метод для while: member this. While(p : unit -> bool, body : unit -> Stack<_>) = if p() then this. Combine( body(), fun () -> this. While(p, body)) else new Stack<_>() while expr do cexpr b. While ((fun () -> expr), b. Delay (fun () -> «cexpr» ))

Остальные методы определяются тривиально: member this. Zero () = new Stack<_>() member this. Try. Остальные методы определяются тривиально: member this. Zero () = new Stack<_>() member this. Try. With(try. Block : unit -> Stack<_>, handler : exn -> Stack<_>) = try. Block() with | e -> handler e member this. Try. Finally(try. Block : unit -> Stack<_>, finalizer : unit -> unit) = try. Block() finally finalizer() member this. For(coll : 'a seq, body : ('a -> Stack<_>)) = let s = new Stack<_>() for elem in coll do for generated in (body elem) do s. Push generated s

Теперь мы можем использовать всё подмножество F#, доступное внутри вычислительных выражений в наших stack Теперь мы можем использовать всё подмножество F#, доступное внутри вычислительных выражений в наших stack expressions: let rec Stack. Count n = stack { if n > 0 then yield! (Stack. Count (n - 1)) }

State workflow Рассмотрим задачу нумерации листьев бинарного дерева: type Bin. Tree<'a> = | Leaf State workflow Рассмотрим задачу нумерации листьев бинарного дерева: type Bin. Tree<'a> = | Leaf of 'a | Node of Bin. Tree<'a> * Bin. Tree<'a> let tree = Node( Leaf "Pikachu", Leaf "Raichu"), Leaf "Psyduck"), . . . ))

Branch: Leaf: Branch: Leaf: "Pikachu" Leaf: "Raichu" Leaf: "Psyduck" Branch: Leaf: "Bulbasaur" Branch: Leaf: "Slowpoke" Leaf: "Slowbro"

Мы хотим получить такое дерево: Branch: Leaf: ( Мы хотим получить такое дерево: Branch: Leaf: ("Pikachu", 0) Leaf: ("Raichu", 1) Leaf: ("Psyduck", 2) Branch: Leaf: ("Bulbasaur", 3) Branch: Leaf: ("Slowpoke", 4) Leaf: ("Slowbro", 5)

Можно просто использовать изменяемое глобальное состояние: let label tree = let label = ref Можно просто использовать изменяемое глобальное состояние: let label tree = let label = ref -1 let rec helper = function | Leaf v -> label : = !label + 1 Leaf (v, !label) | Node(l, r) -> Node(helper l, helper r) helper tree

Чтобы избежать проблем, связанных с подобными сайд-эффектами, воспользуемся функциональным подходом, явно передавая состояние в Чтобы избежать проблем, связанных с подобными сайд-эффектами, воспользуемся функциональным подходом, явно передавая состояние в аргументах: let label' tree = let rec helper state = function | Leaf v -> state + 1, Leaf (v, state) | Node(l, r) -> let state. L, l' = helper state l let state. R, r' = helper state. L r //(*) state. L, Node(l', r') helper 0 tree

Для того, чтобы выразить функцию, зависящую от состояния, мы меняем её тип с Bin. Для того, чтобы выразить функцию, зависящую от состояния, мы меняем её тип с Bin. Tree<'a> -> Bin. Tree<'a, int> на 'state -> Bin. Tree<'a> -> Bin. Tree<'a, 'b> чтобы менять состояние: 'state -> Bin. Tree<'a> -> 'state * Bin. Tree<'a, 'b>

Проблема заключается в том, что передавать состояние непосредственно — очень неудобно и громоздко. В Проблема заключается в том, что передавать состояние непосредственно — очень неудобно и громоздко. В идеале этот код выглядел бы так: let rec label. Monadic tree = state { match tree with | Leaf v -> let! state = get. State do! set. State (state + 1) return Leaf(v, state) | Node(l, r) -> let! l' = label. Monadic l let! r' = label. Monadic r return Node(l', r') }

Монадический тип: ('state -> 'res * 'state) т. е. функция, принимающая некое состояние и Монадический тип: ('state -> 'res * 'state) т. е. функция, принимающая некое состояние и возвращающая результат и новое состояние. Тогда let! будет действовать так: let! res = f. Monadic args cont Bind(f. Monadic args, (fun res -> cont)) Bind : ('state -> 'res * 'state) * ('res -> ('state -> 'res 1 * 'state)) -> ('state -> 'res 1 * 'state)

type State. Builder() = member this. Bind(f, cont) = (fun init. State -> //1 type State. Builder() = member this. Bind(f, cont) = (fun init. State -> //1 let res, new. State = f init. State //2 cont res new. State) //3 1 — возвращаем функцию, зависящую от состояния, возвращающую новое состояние и результат 2 — считаем результат выражения и новое состояние 3 — вызываем континуацию с результатом и новым состояинем let! res = f. . . //cont = (fun res ->. . . )

return просто создаёт ф-цию, возвращающую заданный результат при любом состоянии: member this. Return res return просто создаёт ф-цию, возвращающую заданный результат при любом состоянии: member this. Return res = (fun state -> res, state) //return res Return : : 'res -> ('state -> 'res * 'state)

Неявная передача состояния: let! left' = f left let! right' = f right. . Неявная передача состояния: let! left' = f left let! right' = f right. . . Bind(f left, (fun left' -> Bind(f right, (fun right' -> . . . )))) (fun init. State -> let res, new. State = f left init. State (fun left' ->. . . ) res new. State)

Функция получения состояния: let get. State = (fun state -> state, state) Функция установки Функция получения состояния: let get. State = (fun state -> state, state) Функция установки нового состояния: let set. State new. State = (fun _ -> (), new. State)

Без «сахара» : let! state = get. State do! set. State (state + 1) Без «сахара» : let! state = get. State do! set. State (state + 1) return Leaf(v, state) Bind(get. State, (fun state -> Bind(set. State (state + 1), (fun () -> Return Leaf(v, state))))) Bind(f, cont) = (fun init. State -> let res, new. State = f init. State cont res new. State)

Задание Реализуйте метод для выполнения оператора use в Stack. Expressions. Builder ‘e: member Using Задание Реализуйте метод для выполнения оператора use в Stack. Expressions. Builder ‘e: member Using : resource: 'a when 'a : > IDisposable * cont: ('a -> Stack<_>) -> Stack<_>

Тест: let stack. From. Resource = stack { use reader = new Stream. Reader(@ Тест: let stack. From. Resource = stack { use reader = new Stream. Reader(@"C: . . . ") while not <| reader. End. Of. Stream do yield int <| reader. Read. Line() }

Workflows vs. Monads Выч. выражения аналогичны т. е. монадам из языка Haskell. Монада — Workflows vs. Monads Выч. выражения аналогичны т. е. монадам из языка Haskell. Монада — это тип M<'a> и две операции: bind : M<'T> -> ('T -> M<'U>) -> M<'U> return : 'T -> M<'T> Эти операции аналогичны let! и return в выч. выражениях.

Эти операции должны удовлетворять 3 аксиомам. return аналогично нулевому элементу: bind (return x) f Эти операции должны удовлетворять 3 аксиомам. return аналогично нулевому элементу: bind (return x) f ≡ f x bind m return ≡ m Последовательное связывание с двумя фциями аналогично связыванию с одной функцией, полученной из них: bind (bind m f) g ≡ bind m (λx -> (bind (f x) g))

Если ввести оператор >=> (композиция для монад, оператор композиции Клейсли): f >=> g = Если ввести оператор >=> (композиция для монад, оператор композиции Клейсли): f >=> g = fun x -> monad { let! y = f x return g y } >=> : (‘a -> m ‘b) -> (‘b -> m ‘c) -> (‘a -> m c) то аксиомы запишутся так: 1. «Left identity» : return >=> f ≡ f 2. «Right identity» : f >=> return ≡ f 3. «Ассоциативность» : (f >=> g) >=> h ≡ f >=> (g >=> h)

Тем не менее, workflows отличаются от монад тем, что: • Они могут содержать в Тем не менее, workflows отличаются от монад тем, что: • Они могут содержать в себе сайд-эффекты, не выраженные в типах • Могут быть скомбинированы с quotations • Могут выражать вычисления, генерирующие несколько результатов (моноиды), такие, как seq/list/array/stack. . . expressions. Они обычно содержат методы для yield и yield! вместо return и return! и часто не имеют метода для let!.