Подтвердить что ты не робот

Является ли "с" монадическим?

Как и многие безрассудные пионеры передо мной, я пытаюсь пересечь бескрайние пустыни, которые понимают Монады.

Я все еще ошеломляю, но не могу не заметить некоторого монад-подобного качества в инструкции 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 утра.

4b9b3361

Ответ 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)

Есть еще одна вещь, препятствующая withwithf) быть монадическим связыванием. Значение блока должно быть монадическим типом с конструктором того же типа, что и элемент 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 и запускает выход заявления.

(Там есть еще одна огромная разница, которая заключается в том, что тело должно зависеть от ввод инструкций. Снова я думаю, что это должно быть исправлено).