Бесплатная монада на F # с универсальным типом вывода

Я пытаюсь применить шаблон бесплатной монады, как описано в F # для развлечения и прибыли для реализации доступа к данным (для хранилища таблиц Microsoft Azure)

Пример

Предположим, у нас есть три таблицы базы данных и три dao's Foo, Bar, Baz:

Foo          Bar          Baz

key | col    key | col    key | col
---------    ---------    ---------
foo |  1     bar |  2         |

Я хочу выбрать Foo с key = "foo" и Bar с key = "bar", чтобы вставить Baz с key = "baz" и col = 3

Select<Foo> ("foo", fun foo -> Done foo)
  >>= (fun foo -> Select<Bar> ("bar", fun bar -> Done bar)
    >>= (fun bar -> Insert<Baz> ((Baz ("baz", foo.col + bar.col), fun () -> Done ()))))

В функции интерпретатора

  • Select приводит к вызову функции, которая принимает key : string и возвращает obj
  • Insert приводит к вызову функции, которая принимает obj и возвращает unit

Проблема

Я определил две операции Select и Insert в дополнение к Done, чтобы завершить вычисление:

type StoreOp<'T> =
  | Select of string * ('T -> StoreOp<'T>)
  | Insert of 'T * (unit -> StoreOp<'T>)
  | Done of 'T

Чтобы связать StoreOp в цепочку, я пытаюсь реализовать правильную функцию привязки:

let rec bindOp (f : 'T1 -> StoreOp<'T2>) (op : StoreOp<'T1>) : StoreOp<'T2> =
  match op with
  | Select (k, next) ->
      Select (k, fun v -> bindOp f (next v))
  | Insert (v, next) ->
      Insert (v, fun () -> bindOp f (next ()))
  | Done t ->
      f t

  let (>>=) = bindOp

Однако компилятор f # правильно предупреждает меня, что:

The type variable 'T1 has been constrained to be type 'T2

Для этой реализации bindOp тип фиксируется на протяжении всего вычисления, поэтому вместо:

Foo > Bar > unit

все, что я могу выразить, это:

Foo > Foo > Foo

Как мне изменить определение StoreOp и / или bindOp для работы с разными типами на протяжении всего вычисления?


person dtornow    schedule 15.01.2017    source источник
comment
Я могу указать вам точную причину этой ошибки в вашем bindOp коде, но основная причина - ваш StoreOp тип. Если вы посмотрите на него внимательно, вы увидите, что он может выражать цепочки операций только одного типа.   -  person Fyodor Soikin    schedule 16.01.2017
comment
Разве нельзя было бы избежать всех этих уровней косвенного обращения и выполнить простые CRUD-операции в чем-то вроде транзакции Скрипт? Это похоже на то, что Томас Петричек описывает в последнем абзаце своего ответа. См. Также Почему бесплатная Monad не бесплатна.   -  person Nikos Baxevanis    schedule 16.01.2017
comment
Текущая реализация представляет собой простой набор императивных функций CRUD. Пожалуйста, смотрите комментарий ниже для мотивации.   -  person dtornow    schedule 16.01.2017


Ответы (1)


Как заметил в комментариях Федор, проблема в объявлении типа. Если вы хотите заставить его компилироваться ценой принесения в жертву безопасности типов, вы можете использовать obj в двух местах - это, по крайней мере, показывает, в чем проблема:

type StoreOp<'T> =
  | Select of string * (obj -> StoreOp<'T>)
  | Insert of obj * (unit -> StoreOp<'T>)
  | Done of 'T

Я не совсем уверен, что эти две операции должны моделировать, но я полагаю, Select означает, что вы что-то читаете (с помощью клавиши string?), А Insert означает, что вы сохраняете какое-то значение (а затем продолжаете с unit). Итак, здесь данные, которые вы храните / читаете, будут obj.

Есть способы сделать этот тип безопасным, но я думаю, вы получите лучший ответ, если объясните, чего вы пытаетесь достичь, используя монадическую структуру.

Не зная больше, я думаю, что использование бесплатных монад только сделает ваш код очень запутанным и трудным для понимания. F # - это язык в первую очередь функциональность, что означает, что вы можете писать преобразования данных в приятном функциональном стиле, используя неизменяемые типы данных, и использовать императивное программирование для загрузки данных и сохранения результатов. Если вы работаете с табличным хранилищем, почему бы просто не написать обычный императивный код для чтения данных из табличного хранилища, передать результаты в чистое функциональное преобразование, а затем сохранить результаты?

person Tomas Petricek    schedule 16.01.2017
comment
Спасибо за ваш ответ. Чтобы ответить на ваш вопрос: я пытаюсь разделить чистый и нечистый код, как описано в Чистота на нечистом языке. Вы упомянули, что есть способы сделать тип решения obj безопасным. Не могли бы вы поделиться своим подходом к этому? - person dtornow; 16.01.2017
comment
Я согласен с тем, что разделение чистого и нечистого кода желательно, но бесплатная монада - ужасный способ сделать это. В конце концов, код в любом случае будет нечистым - бесплатная монада позволяет вам абстрагироваться от того, как именно обрабатываются примеси, - и я думаю, что в этом нет никакой пользы. (Вы могли бы возразить, что это полезно для тестирования, но я думаю, что это просто скрывает тестирование ключевой части, которую вы должны тестировать, а именно операции с данными.) Если есть конкретная вещь, которую вы хотите достичь, тогда есть лучший вариант. способ сделать это. - person Tomas Petricek; 16.01.2017
comment
Что касается типобезопасной версии, вы получите тип, который статически отслеживает типы всех операций чтения и записи таких M<Read<int,<Write<string,<Read<int, Return<unit>>>>>>> Пример проекта, который использует это: blumu.github.io/ResumableMonad - person Tomas Petricek; 16.01.2017
comment
Интересно, я попробую. Отказ от ответственности: мнение, анекдотично. В прошлом у меня был хороший опыт работы со свободными монадами, однако до сих пор я не сталкивался с необходимостью в общих типах вывода. Тестируемость - аспект, часто упоминаемый в литературе. Вдобавок мне нравится тот факт, что детали реализации, такие как использование Option или Choice, ведение журнала или локальное состояние, аккуратно расположены только в функции интерпретатора. Также выполнение нечистого кода отодвигается до границы выполнения приложения. В настоящее время я пытаюсь сравнить решение CRUD с бесплатным решением для монад. - person dtornow; 16.01.2017