Питон "с" монадным? - PullRequest
       11

Питон "с" монадным?

34 голосов
/ 20 августа 2011

Как и многие безрассудные пионеры до меня, я стараюсь пересечь безлюдную пустошь под названием «Понимание монад».

Я все еще поражаюсь, но не могу не заметить, что какая-то монадакачество о with утверждении Python.Рассмотрим этот фрагмент:

with open(input_filename, 'r') as f:
   for line in f:
       process(line)

Рассмотрим вызов open() как «единицу», а сам блок как «связывание».Настоящая монада не выставлена ​​(э-э, если f не является монадой), но шаблон есть.Не так ли?Или я просто принимаю все FP за монадры?Или это только 3 часа утра и что-то кажется правдоподобным?

Смежный вопрос: если у нас есть монады, нужны ли нам исключения?

В приведенном выше фрагменте любой сбой в I /O может быть скрыт от кода.Повреждение диска, отсутствие именованного файла и пустой файл могут рассматриваться одинаково.Так что нет необходимости в видимом исключении ввода-вывода.

Конечно, класс типов Option в Scala устранил страшный Null Pointer Exception.Если вы переосмыслили числа как монады (с NaN и DivideByZero в качестве особых случаев) ...

Как я уже сказал, 3 часа утра.

Ответы [ 4 ]

23 голосов
/ 20 августа 2011

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

Причина, по которой вам нужно сделать эти тонкие различия, заключается в том, что если вы ошибочно считаете withmonadic, вы в конечном итоге будете злоупотреблять им и писать программы, которые не будут работать из-за ошибок типа.Другими словами, вы будете писать мусор.Что вам нужно сделать, это отличить конструкцию, которая является конкретной вещью (например, монадой), от конструкции, которая может использоваться таким же образом (например, опять же, монадой).Последнее требует дисциплины со стороны программиста или определения дополнительных конструкций для обеспечения дисциплины.Вот почти монадическая версия with (тип M a -> (a -> b) -> M b):

def withm(context, f):
    with context as x:
        return type(context)(f(x))

В конечном счете вы можете считать with комбинатором, но более общим, чемкомбинатор, необходимый для монад (который является связующим).Может быть больше функций, использующих монады, чем две требуемые (монада списка также имеет cons, append и length, например), поэтому, если вы определили соответствующий оператор связывания для менеджеров контекста (например, withm), тогда with можетбыть монадическим в смысле вовлечения монад.

10 голосов
/ 20 августа 2011

Да.

Прямо под определением, Википедия говорит :

В терминах объектно-ориентированного программирования конструкция типабудет соответствовать объявлению монадического типа, функция модуля принимает роль метода-конструктора, а операция привязки содержит логику, необходимую для выполнения ее зарегистрированных обратных вызовов (монадических функций).

Thisзвучит для меня так же, как протокол менеджера контекста, реализация протокола менеджера контекста объектом и оператор with.

От @Owen в комментарии к этому сообщению:

На самом базовом уровне монады - более или менее крутой способ использовать стиль прохождения продолжения: >> = принимает «продюсер» и «обратный вызов»;это также в основном то, что есть с: производитель, такой как open (...) и блок кода, который будет вызван после его создания.

Полное определение Википедии:

Конструкция типа, которая определяет для каждого базового типа, как получить соответствующий монадический тип.В нотации Хаскелла имя монады представляет конструктор типа.Если M - это имя монады, а t - это тип данных, тогда «M t» - это соответствующий тип в монаде.

Это похоже на протокол диспетчера контекста мне.

Единичная функция, которая отображает значение в базовом типе в значение в соответствующем монадическом типе.Результатом является «простейшее» значение в соответствующем типе, которое полностью сохраняет исходное значение (простота понимается соответствующим образом для монады).В Haskell эта функция называется return из-за того, как она используется в нотации do, описанной ниже.Функция модуля имеет полиморфный тип t → M t.

Фактическая реализация протокола диспетчера контекста объектом.

Операция привязки полиморфного типа (Mt) → (t → M u) → (M u), который Хаскель представляет инфиксным оператором >> =.Его первым аргументом является значение в монадическом типе, вторым аргументом является функция, которая отображает базовый тип первого аргумента в другой монадический тип, и его результатом является этот другой монадический тип.

Это соответствует оператору with и его комплекту.

Так что да, я бы сказал, что with - это монада.Я искал PEP 343 и все связанные отклоненные и отозванные PEP, и ни один из них не упомянул слово "монада".Это, конечно, применимо, но кажется, что goal в операторе with было управление ресурсами, и монада - просто полезный способ получить его.

8 голосов
/ 21 августа 2011

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 вы можете использовать >> вместо>>= (или блок do без стрелки <-).

Поэтому я отвечу на этот вопрос: withFile монаден?

Можно подумать, что он можетбыть написано так:

do f <- withFile "file1" WriteMode
   g <- withFile "file2" ReadMode
   k <- hGetLine g
   hPutStr f k

Но это не проверка типа.И это не может.

Это потому, что в Haskell монада ввода-вывода является последовательной : если вы пишете

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's 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 использует исключения, чтобы избежать этого.

3 голосов
/ 21 августа 2011

Я слишком долго думал об этом, и я считаю, что ответ «да, когда он используется определенным образом» (спасибо outis :), но не по той причине, о которой я думал раньше.

Я упомянул в комментарии к ответу agf , что >>= это просто стиль передачи продолжения - дать ему производителя и обратный вызов, и он "запускает" производителя и передает его Перезвоните. Но это не совсем так. Также важно, что >>= должен работать некоторое взаимодействие между производителем и результатом обратного вызова.

В случае монады Список, это будет объединение списков. это именно взаимодействие делает монады особенными.

Но я считаю, что Python with делает такое взаимодействие, только не в как и следовало ожидать.

Вот пример программы на python, использующей два оператора with:

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"]

И порядок тот же.

Аналогия между двумя программами очень прямая: у Context есть некоторые «входящие» биты, «тело» и некоторые «выходящие» биты. Я использовал String вместо IO действий, потому что это проще - я думаю, что это должно быть похоже на IO действий (поправьте меня, если это не так).

И >>= для Context делает точно что делает with в Python: он запускает вводит операторы, передает значение в body и запускает выход заявления.

(Есть еще одна огромная разница в том, что тело должно зависеть от входящие заявления. Я снова думаю , что должно быть исправлено).

...