Создание вычислительного выражения «добавить»

#f# #computation-expression

#f# #вычисление-выражение

Вопрос:

Я бы хотел, чтобы приведенное ниже примерное вычислительное выражение и значения возвращали 6. Для некоторых чисел результаты не такие, как я ожидал. Какой шаг мне не хватает, чтобы получить мой результат? Спасибо!

 type AddBuilder() =
    let mutable x = 0
    member _.Yield i = x <- x   i
    member _.Zero() = 0
    member _.Return() = x

let add = AddBuilder()

(* Compiler tells me that each of the numbers in add don't do anything
   and suggests putting '|> ignore' in front of each *)
let result = add { 1; 2; 3 }

(* Currently the result is 0 *)
printfn "%i should be 6" result
 

Примечание: это только для создания моего собственного вычислительного выражения, чтобы расширить мое обучение. Seq.sum было бы лучшим подходом. Я согласен с идеей, что в этом примере полностью отсутствует значение вычислительных выражений и он не подходит для обучения.

Ответ №1:

Здесь много неправильного.

Во-первых, давайте начнем с простой механики.

Для Yield вызова метода код внутри фигурных скобок должен использовать yield ключевое слово:

 let result = add { yield 1; yield 2; yield 3 }
 

Но теперь компилятор будет жаловаться, что вам также нужен Combine метод. Видите ли, семантика yield заключается в том, что каждое из них производит законченное вычисление, результирующее значение. И поэтому, если вы хотите иметь более одного, вам нужен какой-то способ «склеить» их вместе. Это то, что Combine делает метод.

Поскольку ваш конструктор вычислений фактически не дает никаких результатов, а вместо этого изменяет свою внутреннюю переменную, конечным результатом вычисления должно быть значение этой внутренней переменной. Итак, это то, что Combine нужно вернуть:

 member _.Combine(a, b) = x
 

Но теперь компилятор снова жалуется: вам нужен Delay метод. Delay это не является строго необходимым, но это необходимо для того, чтобы смягчить недостатки производительности. Когда вычисление состоит из множества «частей» (как в случае нескольких yield s), часто бывает так, что некоторые из них следует отбросить. В такой ситуации было бы неэффективно оценивать их все, а затем отбрасывать некоторые. Итак, компилятор вставляет вызов Delay : он получает функцию, которая при вызове будет оценивать «часть» вычислений, и Delay имеет возможность поместить эту функцию в какой-то отложенный контейнер, чтобы позже Combine можно было решить, какой из этих контейнеров отбросить, а какой оценить.

В вашем случае, однако, поскольку результат вычисления не имеет значения (помните: вы не возвращаете никаких результатов, вы просто изменяете внутреннюю переменную), вы Delay можете просто выполнить функцию, которую она получает, чтобы она вызывала побочные эффекты (которые — изменение переменной):

 member _.Delay(f) = f ()
 

И теперь вычисление, наконец, компилируется, и вот: его результат таков 6 . Этот результат исходит от того, что Combine возвращается. Попробуйте изменить его следующим образом:

 member _.Combine(a, b) = "foo"
 

Теперь внезапно результат ваших вычислений становится "foo" .


А теперь давайте перейдем к семантике.

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

Предполагается, что у строителя не должно быть никакого внутреннего состояния. Вместо этого предполагается, что его методы манипулируют какими-то сложными значениями, некоторые методы создают новые значения, некоторые изменяют существующие. Например, seq конструктор 1 манипулирует последовательностями. Это тип значений, которые он обрабатывает. Различные методы создают новые последовательности ( Yield ) или преобразуют их каким-либо образом (например Combine , ), и конечным результатом также является последовательность.

В вашем случае, похоже, что значения, с которыми ваш конструктор должен манипулировать, — это числа. И конечным результатом также будет число.

Итак, давайте посмотрим на семантику методов.

Предполагается, Yield что метод создает одно из тех значений, которыми вы манипулируете. Поскольку ваши значения являются числами, это то, что Yield должно возвращаться:

 member _.Yield x = x
 

Предполагается, что Combine метод, как объяснено выше, объединяет два таких значения, которые были созданы разными частями выражения. В вашем случае, поскольку вы хотите, чтобы конечным результатом была сумма, это то, что Combine должно делать:

 member _.Combine(a, b) = a   b
 

Наконец, Delay метод должен просто выполнить предоставленную функцию. В вашем случае, поскольку ваши значения являются числами, нет смысла отбрасывать какие-либо из них:

 member _.Delay(f) = f()
 

И это все! С помощью этих трех методов вы можете добавлять числа:

 type AddBuilder() =
    member _.Yield x = x
    member _.Combine(a, b) = a   b
    member _.Delay(f) = f ()

let add = AddBuilder()

let result = add { yield 1; yield 2; yield 3 }
 

Я думаю, что числа не очень хороший пример для изучения вычислительных выражений, потому что числам не хватает внутренней структуры, которую должны обрабатывать вычислительные выражения. Попробуйте вместо этого создать maybe конструктор для управления Option<'a> значениями.

Дополнительный бонус — уже есть реализации, которые вы можете найти в Интернете и использовать для справки.


1 seq на самом деле не является вычислительным выражением. Оно предшествует вычислительным выражениям и обрабатывается компилятором особым образом. Но достаточно хорошо для примеров и сравнений.

Комментарии:

1. Идеальный. В случае, если кто-то другой, похожий на мой уровень понимания вычислительных выражений, прочитает это, предложение The builder не должно иметь никакого внутреннего состояния. для меня было действительно важно осознать, и на случай, если они пропустили это в этом посте, я хотел бы назвать это здесь.