В этом посте мы отклонимся от основной темы и познакомимся с парой трюков, которые помогут вам разнообразить методы в построителе вычислительных выражений.
В конечном итоге наши исследования заведут нас в тупик, но я надеюсь, что путешествие станет источником прозрений, как правильно проектировать вычислительные выражения.
Обратите внимание, что "построитель" в контексте вычислительных выражений — это не то же самое, что объектно-ориентированный паттерн "строитель", который используется для конструирования и валидации объектов.
Озарение: методы построителя можно перегружать
В какой-то момент у вас может возникнуть озарение:
- Методы построителя — обычные методы класса, и, в отличие от автономной функций, методы поддерживают перегрузку с отличающимися типами параметров. Это значит, что мы можем делать отличающиеся реализации, если типы их параметров отличаются. 
Возможно, эта идея вас вдохновляет и вы с энтузиазмом придумываете, как можно её использовать.
 Однако, может оказаться, что она не настолько полезна, насколько вы думаете.
 Взглянем на несколько примеров.
Перегрузка "Return"
Скажем, у вас есть тип-объединение.
 Вы решаете перегрузить Return или Yield с разными реализациями для каждого варианта объединения.
Вот очень простой пример, где Return имеет две перегрузки:
type SuccessOrError =
| Success of int
| Error of string
type SuccessOrErrorBuilder() =
    member this.Bind(m, f) =
        match m with
        | Success s -> f s
        | Error _ -> m
    /// перегрузка для int
    member this.Return(x:int) =
        printfn "Return a success %i" x
        Success x
    /// перегрузка для strings
    member this.Return(x:string) =
        printfn "Return an error %s" x
        Error x
// создаём экземпляр процесса
let successOrError = new SuccessOrErrorBuilder()
А вот как его можно использовать:
successOrError {
    return 42
    } |> printfn "Результат в случае успеха: %A"
// Результат в случае успеха: Success 42
successOrError {
    return "error for step 1"
    } |> printfn "Результат в случае ошибки: %A"
// Результат в случае ошибки: Error "error for step 1"
Как вы думаете, что здесь не так?
Ну, во-первых, если мы вернёмся к обсуждению типов-обёрток, то обнаружим, что типы-обёртки лучше делать обобщёнными.
 Мы хотим, насколько это возможно, использовать наш код повторно, в том числе, и процессы. Зачем привязывать реализацию к какому-то конкретному примитивному типу?
В нашем случае это значит, что тип объединения должны быть обобщён:
type SuccessOrError<'a,'b> =
| Success of 'a
| Error of 'b
Но теперь, из-за обобщённых типов, метод Return больше не может быть перегружен! (В случае, если 'a и 'b совпадают, возникнет неоднозначность — прим. переводчика.)
Во-вторых, не самая хорошая идея — раскрывать детали реализации типа внутри выражения.
 Концепция вариантов "успех" и "неудача" полезна, но лучшим способом было бы скрыть вариант "неудачи" и обрабатывать его автоматически внутри Bind:
type SuccessOrError<'a,'b> =
| Success of 'a
| Error of 'b
type SuccessOrErrorBuilder() =
    member this.Bind(m, f) =
        match m with
        | Success s ->
            try
                f s
            with
            | e -> Error e.Message
        | Error _ -> m
    member this.Return(x) =
        Success x
// создаём экземпляр процесса
let successOrError = new SuccessOrErrorBuilder()
Здесь Return используется только в случае "успеха", а "неудача" спрятана.
successOrError {
    return 42
    } |> printfn "Результат в случае успеха: %A"
successOrError {
    let! x = Success 1
    return x/0
    } |> printfn "Результат в случае ошибки: %A"
Больше примеров этой техники мы увидим в следующем посте.
Множество реализаций "Combine"
Другая ситуация, когда у вас может возникнуть соблазн перегрузить метод — это Combine.
Для примера перепишем метод Combine для процесса trace.
 Если вы помните, в предыдущей реализации мы просто складывали числа.
Но что, если мы изменим наши требования, скажем, так:
- если мы возвращаем несколько значений в процессе - trace, мы объединяем их в список.
Первая попытка переписать Combine будет выглядеть так:
member this.Combine (a,b) =
    match a,b with
    | Some a', Some b' ->
        printfn "комбинируем %A с %A" a' b'
        Some [a';b']
    | Some a', None ->
        printfn "комбинируем %A с None" a'
        Some [a']
    | None, Some b' ->
        printfn "комбинируем None с %A" b'
        Some [b']
    | None, None ->
        printfn "комбинируем None с None"
        None
В методе Combine мы разворачиваем значения из переданного опционального типа с комбинируем их в список-обёртку в Some (т.е. Some [a';b']).
Для двух операторов yield всё будет работать, как мы и ожидали:
trace {
    yield 1
    yield 2
    } |> printfn "Результат yield и последющий yield: %A"
// Результат yield и последющий yield: Some [1; 2]
И при возврате None, всё тоже будет работать как надо:
trace {
    yield 1
    yield! None
    } |> printfn "Результат yield и последующий None: %A"
// Результат yield и последующий None: Some [1]
Но что получится, если мы попробуем скомбинировать три значения? Например, так:
trace {
    yield 1
    yield 2
    yield 3
    } |> printfn "Результат yield x 3: %A"
Внезапно мы получим ошибку компиляции:
Error FS0193 : Несоответствие ограничений типов. Тип 
    "int list option"    
несовместим с типом
    "int option"
В чём проблема?
Ответ в том, что после комбинирования 2-го и 3-го значений (yield 2; yield 3), мы получаем опциональное значение, содержащее список целых или int list option.
 Ошибка происходит, когда мы пытаемся комбинировать первое значение (Some 1) с комбинированным значением (Some [2;3]).
 То есть мы передаём int list option в качестве второго параметра Combine, но первый параметр всё ещё обычный int option.
 Компилятор говорит нам, что он хочет, чтобы второй параметр имел такой же тип, как и первый.
Мы снова можем использовать трюк с перегрузкой.
 Напишем две реализации Combine с различными типами второго параметра — одну, которая принимает int option и вторую которая принимает int list option.
Вот два метода с различными типами параметра:
/// комбинируем с опциональным списком
member this.Combine (a, listOption) =
    match a,listOption with
    | Some a', Some list ->
        printfn "комбинируем %A с %A" a' list
        Some ([a'] @ list)
    | Some a', None ->
        printfn "комбинируем %A с None" a'
        Some [a']
    | None, Some list ->
        printfn "комбинируем None с %A" list
        Some list
    | None, None ->
        printfn "комбинируем None с None"
        None
/// комбинируем с опциональным одиночным значением
member this.Combine (a,b) =
    match a,b with
    | Some a', Some b' ->
        printfn "комбинируем %A с %A" a' b'
        Some [a';b']
    | Some a', None ->
        printfn "комбинируем %A с None" a'
        Some [a']
    | None, Some b' ->
        printfn "комбинируем None с %A" b'
        Some [b']
    | None, None ->
        printfn "комбинируем None с None"
        None
Теперь мы можем скомбинировать три значения и получим именно то, что хотели.
trace {
    yield 1
    yield 2
    yield 3
    } |> printfn "Результат yield x 3: %A"
// Результат yield x 3: Some [1; 2; 3]
К сожалению, этот трюк сломал часть предыдущего кода!
 Если сейчас вы попробуете вернуть None, вы получите ошибку компиляции.
trace {
    yield 1
    yield! None
    } |> printfn "Результа yield с последующим None: %A"
Ошибка:
Невозможно определить уникальную перегрузку метода "Combine" на основе сведений о типе, заданных до данной точки программы. Возможно, требуется аннотация типа.
Прежде чем впасть в раздражение, попробуйте думать как компилятор.
 Если бы вы были компилятором, и получили бы None, какой бы метод вызвали вы?
Правильного ответа нет, потому что None можно передать вторым параметром в оба метода.
 Компилятор не знает, относится ли этот None к типу int list option (первый метод) или к типу int option (второй метод).
Как и говорит компилятор, нам помогла бы аннотация типа, поэтому давайте её и предоставим. Заставим None быть int option.
trace {
    yield 1
    let x:int option = None
    yield! x
    } |> printfn "Результат yield с последующим None: %A"
Конечно, это уродливо, но на практике встречается не слишком часто.
Гораздо важнее, что подобное уродство — признак плохого дизайна.
 Иногда вычислительное выражение возвращает 'a option, а иногда — 'a list option.
 Мы должны быть последовательны в своём дизайне, так что вычислительное выражение всегда должно иметь один и тот же тип, независимо от того, сколько в нём встретилось операторов yield.
Если мы действительно хотим разрешить сколько угодно операторов yield, то в качестве типа-обёртки мы с самого начала должны использовать 'a list option.
 В этом случае метод Yield будет создавать list option, а метод Combine снова получит только одну реализацию.
Вот третья версия нашего кода.
type TraceBuilder() =
    member this.Bind(m, f) =
        match m with
        | None ->
            printfn "Bind с None. Выход."
        | Some a ->
            printfn "Bind с Some(%A). Продолжаем" a
        Option.bind f m
    member this.Zero() =
        printfn "Zero"
        None
    member this.Yield(x) =
        printfn "Yield незавёрнутого %A" x
        Some [x]
    member this.YieldFrom(m) =
        printfn "Yield завёрнутого (%A)" m
        m
    member this.Combine (a, b) =
        match a,b with
        | Some a', Some b' ->
            printfn "комбинируем %A с %A" a' b'
            Some (a' @ b')
        | Some a', None ->
            printfn "комбинируем %A с None" a'
            Some a'
        | None, Some b' ->
            printfn "комбинируем None с %A" b'
            Some b'
        | None, None ->
            printfn "комбинируем None с None"
            None
    member this.Delay(f) =
        printfn "Delay"
        f()
// создаём экземпляр процесса
let trace = new TraceBuilder()
Теперь пример работает так, как ожидается, без всяких особых трюков:
trace {
    yield 1
    yield 2
    } |> printfn "Результат yield с последующим yield: %A"
// Результат yield с последующим yield: Some [1; 2]
trace {
    yield 1
    yield 2
    yield 3
    } |> printfn "Результат yield x 3: %A"
// Результат yield x 3: Some [1; 2; 3]
trace {
    yield 1
    yield! None
    } |> printfn "Результат yield с последующим None: %A"
// Результат yield с последующим None: Some [1]
Этот код не только чище, но универсальней, поскольку вместо конкретного типа int option мы используем обобщённый тип 'a option. То же самое ранее нам удалось сделать c методом Return.
Перегрузка "For"
Разумный вариант, где перегрузка может оказаться полезной — метод For.
 Возможные причины:
- Вы хотите поддерживать различные типы коллекций (т.е. - listи- IEnumerable).
- Вы хотите реализовать эффективный обход для определённых типов коллекций. 
- Вы хотите «обернуть» список в другой тип (скажем, сделать - LazyList) и собираетесь поддерживать обе версии — завёрнутую и незавёрнутую.
Вот пример построителя списков, который поддерживает не только списки, но и последовательности:
type ListBuilder() =
    member this.Bind(m, f) =
        m |> List.collect f
    member this.Yield(x) =
        printfn "Yield незавёрнутого %A" x
        [x]
    member this.For(m,f) =
        printfn "For %A" m
        this.Bind(m,f)
    member this.For(m:_ seq,f) =
        printfn "For %A используя seq" m
        let m2 = List.ofSeq m
        this.Bind(m2,f)
// создаём экземпляр процесса
let listbuilder = new ListBuilder()
А вот как его использовать:
listbuilder {
    let list = [1..10]
    for i in list do yield i
    } |> printfn "Результат для list: %A"
listbuilder {
    let s = seq {1..10}
    for i in s do yield i
    } |> printfn "Результат для seq : %A"
Если вы закомментируете второй метод For, пример с последовательностью начнёт приводить к ошибке компиляции.
 Так что в данном случае перегрузка нужна.
Заключение
Что ж, мы узнали, что методы можно перегружать, если нужно, но надо быть осторожными, прежде, чем бросаться в подобного рода решения с головой , потому что потребность в таком коде может быть признаком слабого дизайна.
В следующем посте мы вернёмся к основной теме и познакомимся с тонкой настройкой процесса вычисления выражений, используя отложенные вычисления вне построителя.
 
          