Является ли "с" монадическим?
Как и многие безрассудные пионеры передо мной, я пытаюсь пересечь бескрайние пустыни, которые понимают Монады.
Я все еще ошеломляю, но не могу не заметить некоторого монад-подобного качества в инструкции Python with
. Рассмотрим этот фрагмент:
with open(input_filename, 'r') as f:
for line in f:
process(line)
Рассмотрим вызов open() как "unit" и сам блок как "bind". Фактическая монада не отображается (мм, если f
- монада), но шаблон есть. Не так ли? Или я просто принимаю все FP за монады? Или это всего лишь 3 часа ночи, и что-то кажется правдоподобным?
Связанный вопрос: если у нас есть монады, нужны ли нам исключения?
В приведенном выше фрагменте любая ошибка в операции ввода-вывода может быть скрыта от кода. Повреждение диска, отсутствие именованного файла и пустой файл можно обрабатывать одинаково. Поэтому нет необходимости в видимом исключении IO.
Конечно, Scala Option
typeclass устранил опасное исключение Null Pointer. Если вы переосмысливаете числа как Monads (с NaN и DivideByZero в качестве особых случаев)...
Как я уже сказал, 3 утра.
Ответы
Ответ 1
Да.
Прямо под определением Википедия говорит:
В объектно-ориентированных терминах программирования построение типа должно соответствовать объявлению монадического типа, функция единиц принимает роль метода-конструктора, а операция привязки содержит логику, необходимую для выполнения своих зарегистрированных обратных вызовов (монадическое функции).
Это звучит для меня точно так же, как протокол диспетчера контекста, реализация протокола контекстного менеджера объектом и оператор with
.
От @Owen в комментарии к этой записи:
Монады на самом базовом уровне - это более или менее классный способ использования стиля продолжения: → = принимает "продюсер" и "обратный вызов"; это также в основном то, что с: производитель, такой как open (...) и блок кода, который вызывается после его создания.
Полное определение Википедии:
Конструкция типа, которая определяет для каждого базового типа, как получить соответствующий монадический тип. В нотации Haskell имя монады представляет конструктор типа. Если M - это имя монады, а t - тип данных, то в монаде "M t" - соответствующий тип.
Это звучит как протокол контекстного менеджера для меня.
Единичная функция, которая отображает значение в базовом типе на значение в соответствующем монадическом типе. Результатом является "самое простое" значение соответствующего типа, которое полностью сохраняет исходное значение (простота понимается соответствующим образом монаде). В Haskell эта функция называется return из-за того, как она используется в описательной записи, описанной ниже. Единичная функция имеет полиморфный тип t → M t.
Фактическая реализация протокола-менеджера контекста объектом.
Операция связывания полиморфного типа (M t) → (t → M u) → (M u), которую Haskell представляет оператором infix → =. Его первый аргумент - это значение в монадическом типе, его второй аргумент - это функция, которая отображает из базового типа первого аргумента в другой монадический тип, а его результат - в другом монадическом типе.
Это соответствует оператору with
и его набору.
Итак, я бы сказал, что with
- монада. Я искал PEP 343 и все связанные с этим отвергнутые и отозванные PEP, и ни один из них не упомянул слово "монада". Это, безусловно, относится, но кажется, что цель оператора with
- управление ресурсами, а монада - это просто полезный способ ее получить.
Ответ 2
Это почти тривиально, но первая проблема заключается в том, что with
не является функцией и не принимает функцию в качестве аргумента. Вы можете легко обойти это, написав оболочку функции для with
:
def withf(context, f):
with context as x:
f(x)
Так как это так тривиально, вы не могли бы отличить withf
и with
.
Вторая проблема с with
, являющаяся монадой, состоит в том, что в качестве оператора, а не выражения, оно не имеет значения. Если вы можете указать тип, это будет M a -> (a -> None) -> None
(на самом деле это тип withf
выше). Говоря практически, вы можете использовать Python _
, чтобы получить значение для оператора with
. В Python 3.1:
class DoNothing (object):
def __init__(self, other):
self.other = other
def __enter__(self):
print("enter")
return self.other
def __exit__(self, type, value, traceback):
print("exit %s %s" % (type, value))
with DoNothing([1,2,3]) as l:
len(l)
print(_ + 1)
Так как withf
использует функцию, а не блок кода, альтернативой _
является возврат значения функции:
def withf(context, f):
with context as x:
return f(x)
Есть еще одна вещь, препятствующая with
(и withf
) быть монадическим связыванием. Значение блока должно быть монадическим типом с конструктором того же типа, что и элемент with
. Как бы то ни было, with
является более общим. Учитывая agf, обратите внимание, что каждый интерфейс является конструктором типа, я привязываю тип with
как M a -> (a -> b) -> b
, где M - интерфейс менеджера контекста (методы __enter__
и __exit__
). Между типами bind
и with
находится тип M a -> (a -> N b) -> N b
. Чтобы быть монадой, with
пришлось бы сбой во время выполнения, если b
не был M a
. Более того, хотя вы могли бы использовать with
монадически как операцию привязки, это редко имело смысл сделать.
Причина, по которой вам нужно сделать эти тонкие различия, состоит в том, что, если вы ошибочно считаете, что with
является монадическим, вы будете злоупотреблять им и писать программы, которые будут терпеть неудачу из-за ошибок типа. Другими словами, вы напишете мусор. То, что вам нужно сделать, - это отличить конструкцию, которая является определенной вещью (например, монадой) от той, которая может использоваться в манере этой вещи (например, снова монада). Последнее требует дисциплины со стороны программиста или определения дополнительных конструкций для обеспечения соблюдения дисциплины. Здесь почти монадическая версия with
(тип M a -> (a -> b) -> M b
):
def withm(context, f):
with context as x:
return type(context)(f(x))
В конечном итоге вы можете считать with
похожим на комбинатор, но более общим, чем комбинатор, требуемый монадами (который является связыванием). Модады могут быть больше, чем две требуемые (например, в монаде также есть минусы, добавление и длина), поэтому, если вы определили соответствующий оператор привязки для менеджеров контекста (например, withm
), тогда with
может быть монадическим в смысле вовлечения монад.
Ответ 3
Haskell имеет эквивалент with
для файлов, он называется withFile
. Это:
with open("file1", "w") as f:
with open("file2", "r") as g:
k = g.readline()
f.write(k)
эквивалентно:
withFile "file1" WriteMode $ \f ->
withFile "file2" ReadMode $ \g ->
do k <- hGetLine g
hPutStr f k
Теперь withFile
может выглядеть как нечто монадическое. Его тип:
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
Правая сторона выглядит как (a -> m b) -> m b
.
Другое сходство: в Python вы можете пропустить as
, а в Haskell вы можете использовать >>
вместо >>=
(или, <<2 → блока без стрелки <-
).
Итак, я отвечу на этот вопрос: withFile
monadic?
Вы можете подумать, что это можно записать так:
do f <- withFile "file1" WriteMode
g <- withFile "file2" ReadMode
k <- hGetLine g
hPutStr f k
Но это не печатает проверку. И он не может.
Это потому, что в Haskell монашка IO является последовательной: если вы пишете
do x <- a
y <- b
c
после выполнения a
выполняется b
, а затем c
. Нет "обратной линии",
очистить a
в конце или что-то в этом роде. withFile
, с другой стороны,
должен закрыть дескриптор после выполнения блока.
Существует еще одна монада, называемая продолжением монады, которая позволяет делать такие
вещи. Тем не менее, у вас теперь две монады, IO и продолжения, и использование эффектов двух монад одновременно требует использования монадных трансформаторов.
import System.IO
import Control.Monad.Cont
k :: ContT r IO ()
k = do f <- ContT $ withFile "file1" WriteMode
g <- ContT $ withFile "file2" ReadMode
lift $ hGetLine g >>= hPutStr f
main = runContT k return
Это уродливо. Таким образом, ответ: несколько, но для этого требуется иметь дело с множеством тонкостей, которые делают проблему довольно непрозрачной.
Python with
может имитировать только ограниченный бит того, что могут делать монады - добавить код ввода и завершения. Я не думаю, что вы можете имитировать, например.
do x <- [2,3,4]
y <- [0,1]
return (x+y)
с помощью with
(возможно, с некоторыми грязными хаками). Вместо этого используйте для:
for x in [2,3,4]:
for y in [0,1]:
print x+y
И для этого есть функция Haskell - forM
:
forM [2,3,4] $ \x ->
forM [0,1] $ \y ->
print (x+y)
Я рекомендовал прочитать о yield
, который больше похож на монады, чем with
:
http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html
Связанный вопрос: если у нас есть монады, нужны ли нам исключения?
В принципе нет, вместо функции, которая выбрасывает A или возвращает B, вы можете создать функцию, которая возвращает Either A B
. Монада для Either A
будет вести себя точно так же, как исключения - если одна строка кода вернет ошибку, весь блок будет.
Однако это означало бы, что деление будет иметь тип Integer -> Integer -> Either Error Integer
и т.д., чтобы поймать деление на ноль. Вам нужно будет обнаруживать ошибки (явно сопоставление шаблонов или использовать привязку) в любом коде, который использует разделение или имеет даже малейшую вероятность ошибиться. Haskell использует исключения, чтобы избежать этого.
Ответ 4
Я слишком долго думал об этом, и я считаю, что ответ "да, когда он использовал определенный путь" (спасибо outis:),
но не по той причине, о которой я думал раньше.
Я упомянул в комментарии к agf, что >>=
является просто продолжением стиля передачи -
дать ему производителя и обратный вызов, и он "запускает" производителя и подает его на
Перезвони. Но это не совсем так. Также важно, чтобы >>=
выполнялся
некоторое взаимодействие между производителем и результатом обратного вызова.
В случае Монады списка это будет объединение списков. Эта
взаимодействие делает монады особенными.
Но я считаю, что Python with
делает это взаимодействие, просто не в
как вы могли ожидать.
Здесь примерная программа python, использующая два с операторами:
class A:
def __enter__(self):
print 'Enter A'
def __exit__(self, *stuff):
print 'Exit A'
class B:
def __enter__(self):
print 'Enter B'
def __exit__(self, *stuff):
print 'Exit B'
def foo(a):
with B() as b:
print 'Inside'
def bar():
with A() as a:
foo(a)
bar()
При запуске вывод
Enter A
Enter B
Inside
Exit B
Exit A
Теперь Python является обязательным языком, поэтому вместо простого создания данных он
производит побочные эффекты. Но вы можете думать об этих побочных эффектах как о данных
(например, IO ()
) - вы не можете объединить их во всех крутых путях, которые вы могли бы комбинировать
IO ()
, но они достигают той же цели.
Итак, вам следует сосредоточиться на последовательности этих операций, то есть,
порядок заявлений печати.
Теперь сравните ту же программу в Haskell:
data Context a = Context [String] a [String]
deriving (Show)
a = Context ["Enter A"] () ["Exit A"]
b = Context ["Enter B"] () ["Exit B"]
instance Monad Context where
return x = Context [] x []
(Context x1 p y1) >>= f =
let
Context x2 q y2 = f p
in
Context (x1 ++ x2) q (y2 ++ y1)
foo :: a -> Context String
foo _ = b >> (return "Inside")
bar :: () -> Context String
bar () = a >>= foo
main = do
print $ bar ()
Что производит:
Context ["Enter A","Enter B"] "Inside" ["Exit B","Exit A"]
И порядок один и тот же.
Аналогия между двумя программами очень прямая: a Context
имеет некоторые
"enter" бит, "тело" и некоторые "выходящие" биты. Я использовал строки вместо
IO, потому что это проще - я думаю, что это должно быть похоже на действия IO
(исправьте меня, если это не так).
И >>=
для Context
делает именно то, что with
в Python: он запускает
ввод инструкций, передает значение в body
и запускает выход
заявления.
(Там есть еще одна огромная разница, которая заключается в том, что тело должно зависеть от
ввод инструкций. Снова я думаю, что это должно быть исправлено).