Pull to refresh

Как питонистам читать Haskell

Reading time 8 min
Views 7.4K
Original author: Edward Z. Yang
Сталкивались ли вы с тем, что иногда надо быстро понять, что делает кусок кода на неком незнакомом языке? Если язык похож на то, к чему вы привыкли, как правило, можно догадаться о назначении большей части кода — даже если вы не очень хорошо знакомы со всеми фичами языка.
С Haskell все по-другому, так как его синтаксис выглядит совсем иначе, нежели синтаксис традиционных языков. Но, на самом деле, разница не так велика — нужно просто взглянуть под правильным углом. Здесь приводится быстрое, по большей части некорректное, и, надеюсь, полезное руководство по интерпретации питонистами (автор использует слово «Pythonista» — прим. переводчика) кода на Haskell. К концу вы будете способны понять следующий кусок (часть кода опущена за троеточиями):
runCommand env cmd state = ...
retrieveState = ...
saveState state = ...

main :: IO ()
main = do
    args <- getArgs
    let (actions, nonOptions, errors) = getOpt Permute options args
    opts <- foldl (>>=) (return startOptions) actions
    when (null nonOptions) $ printHelp >> throw NotEnoughArguments
    command <- fromError $ parseCommand nonOptions
    currentTerm <- getCurrentTerm
    let env = Environment
            { envCurrentTerm = currentTerm
            , envOpts = opts
            }
    saveState =<< runCommand env command =<< retrieveState


Типы


Игнорируйте все, что стоит после :: (а также игнорируйте type, class, instance и newtype). Некоторые клянутся, что типы помогают им понимать код. Если вы совсем новичок, такие вещи как Int и String, возможно, помогут, а такие, как LayoutClass и MonadError — нет. Не беспокойтесь о них.

Аргументы


f a b c транслируется в f(a, b, c). Haskell опускает скобки и запятые. Одним из следствий этого является то, что иногда нам нужны скобки для аргументов: f a (b1 + b2) c транслируется в f(a, b1 + b2, c).

Символ доллара


Так как сложные выражения вида a + b встречаются довольно часто, а хаскелеры (у автора «Haskellers» — прим. переводчика) недолюбливают скобки, символ доллара используется, чтобы их избегать: f $ a + b эквивалентно f (a + b) и транслируется в f(a + b). Можно считать $ гигантской левой скобкой, которая автоматически закрывается в конце строки (и больше не надо писать ))))), ура!) В частности, вы можете вкладывать их, и каждый создаст уровень вложенности: f $ g x $ h y $ a + b — эквивалентно f (g x (h y (a + b))) и транслируется как f(g(x,h(y,a + b)) (хотя некоторые считают это плохой практикой).
Иногда можно увидеть такой вариант: <$> (с угловыми скобками). Можете считать это тем же самым, что и $. Так же встречается <*> — притворитесь, что это запятая, и f <$> a <*> b транслируется в f(a, b).

Обратные апострофы


x `f` y транслируется в f(x,y). Штука между апострофами — функция, обычно бинарная, а справа и слева — аргументы.

Символ «равно»


Возможны два значения. В начале блока кода он означает, что вы просто определяете функцию:
doThisThing a b c = ...
==>
def doThisThing(a, b, c):
  ...

Рядом с ключевым словом let действует как оператор присваивания:
let a = b + c in ...
==>
a = b + c
  ...


Стрелка влево


Тоже работает как оператор присваивания:
a <- createEntry x
==>
a = createEntry(x)

Почему мы не используем знак равенства? Колдунство. (Если точнее, createEntry x имеет побочные эффекты. Еще точнее — это означает, что выражение — монадическое. Но это все колдунство. Пока не обращайте внимания.)

Стрелка вправо


Все сложно. Вернемся к ним позже.

Ключевое слово do


Шумы. Можно игнорировать. Оно дает некоторую информацию, — что ниже будут побочные эффекты, но вы никогда не увидите разницы в Python.

Return


Шумы. Также игнорировать. (Вы никогда не увидите return, использованный для контроля исполнения.)

Точка


f . g $ a + b транслируется в f(g(a + b)). На самом деле, в программе на Python вы скорее увидите примерно следующее:
x = g(a + b)
y = f(x)
Но у Haskell-программистов аллергия на лишние переменные.

Операторы связывания и рыбка


Можно встретить вещи вроде =<<, >>=, <=< и >=>. Попросту, это еще несколько способов избавиться от промежуточных переменных:
doSomething >>= doSomethingElse >>= finishItUp
==>
x = doSomething()
y = doSomethingElse(x)
finishItUp(y)

Иногда Haskell-программист решает, что красивее сделать это в другом направлении, особенно, если где-то там переменной присваивается значение:
z <- finishItUp =<< doSomethingElse =<< doSomething
==>
x = doSomething()
y = doSomethingElse(x)
z = finishItUp(y)

Самое важное — сделать реверс-инжиниринг происходящего, посмотрев на определения doSomething, doSomethingElse и finishItUp: это даст подсказку, что «протекает» мимо «рыбки». Если вы сделаете это, можно читать <=< и >=> одинаково (на самом деле они осуществляют композицию функций, как .). Читайте >> как точку с запятой (т.е., никакого присваивания):
doSomething >> doSomethingElse
==>
doSomething()
doSomethingElse()


Частичное применение


Иногда Haskell-программисты вызывают функцию, но не передают достаточно аргументов. Не бойтесь, скорее всего, они организовали передачу остальных аргументов в другом месте. Игнорируйте, или ищите функции, которые принимают анонимные функции как аргументы. Обычные подозреваемые: map, fold (и ее варианты), filter, оператор композиции ., оператор «рыбка» (=<<, etc). Это часто случается с числовыми операторами: (+3) транслируется в lambda x: x + 3.

Операторы контроля


Положитесь на инстинкты: эти операторы делают именно, то что вы подумали! (Даже если вы думаете, что они не должны работать так). Так что, если вы видите: when (x == y) $ doSomething x, читайте как «Коль скоро x равен y, вызвать doSomething с аргументом x».
Игнорируйте тот факт, что вы не можете на самом деле транслировать это в when(x == y, doSomething(x)) (тут doSomething будет вызвана в любом случае). На самом деле, более точно будет when(x == y, lambda: doSomething x), но, может быть, более удобно считать when конструкцией языка.
if и case — ключевые слова. Они работают так, как вы ожидаете.

Стрелка вправо (по-настоящему!)


У стрелок вправо нет ничего общего со стрелками влево. Думайте о них, как о двоеточиях: они всегда где-то рядом с ключевым словом case и обратным слэшом (каковой объявляет лямбду: \x -> x транслируется в lambda x: x).
Pattern matching с использованием case — довольно приятная фича, но ее сложно объяснить в этом посте. Возможно, простейшее приближение — это цепочка if..elif..else с назначением переменных:
case moose of
  Foo x y z -> x + y * z
  Bar z -> z * 3
==>
if isinstance(moose, Foo):
  x = moose.x #назначение переменной!
  y = moose.y
  z = moose.z
  return x + y * z
elif isinstance(moose, Bar):
  z = moose.z
  return z * 3
else:
  raise Exception("Pattern match failure!")


Оборачивание


Вы можете отличить обрачивающую функцию по тому, что она начинается с with. Они работают как управление контекстами в Python:
withFile "foo.txt" ReadMode $ \h -> do
  ...
==>
with open("foo.txt", "r") as h:
  ...

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

Исключения


throw, catch, catches, throwIO, finally, handle и все остальные подобные функции работают в точности, как вы ожидаете. Это, однако, может выглядеть забавно, потому что это все функции, а не ключевые слова, со всеми вытекающими. Так, например:
trySomething x `catch` \(e :: IOException) -> handleError e
===
catch (trySomething x) (\(e :: IOException) -> handleError e)
==>
try:
  trySomething(x)
except IOError as e:
  handleError(e)


Maybe


Если вы видите Nothing, можете думать о нем, как о None. Так что isNothing x проверяет, что x is None. Что противоположность Nothing? Just. Например, isJust x проверяет, что x is not None.
Можно увидеть много шумов, связанных с обработкой Just и None в верном порядке. Один из наиболее частых случаев:
maybe someDefault (\x -> ...) mx
==>
if mx is None:
  x = someDefault
else:
  x = mx
  ...

Вот специфический вариант для случая, когда null — это ошибка:
maybe (error "bad value!") (\x -> ...) x
==>
if x is None:
  raise Exception("bad value!")


Записи


Работают, как и ожидается, однако Haskell позволяет вам создавать поля без имени:
data NoNames = NoNames Int Int
data WithNames = WithNames {
  firstField :: Int,
  secondField :: Int
}

Таким образом, NoNames будет представлена в Python тьюплом (1, 2), а WithNames следующим классом:
class WithNames:
  def __init__(self, firstField, secondField):
    self.firstField = firstField
    self.secondField = secondField

Таким простым образом, NoNames 2 3 транслируется в (2, 3), и WithNames 2 3 или WithNames { firstField = 2, secondField = 3 } — в WithNames(2, 3).
Поля несколько отличаются. Самое главное — запомнить, что хаскелеры ставят имена своих полей перед переменной, тогда как вы, скорее всего, привыкли ставить их после. Так что field x транслируется в x.field. Как записать x.field = 2? Что ж, на самом деле, вы не можете сделать этого. Хотя можно скопировать:
return $ x { field = 2 }
==>
y = copy(x)
y.field = 2
return y

Или можно создать с нуля, если заменить x именем структуры данных (она начинается с заглавной буквы). Почему мы позволяем только копировать структуры? Потому что Haskell — это чистый язык; но не обращайте на это внимания. Просто еще одна причуда Haskell.

Списочные выражения


Изначально они пришли из семейства Miranda-Haskell! В них только немного больше символов.
[ x * y | x <- xs, y <- ys, y > 2 ]
==>
[ x * y for x in xs for y in ys if y > 2 ]

Также, оказывается, что хаскелеры часто предпочитают писать списочные выражения в многострочной форме (возможно, они считают, что так их проще читать). Это выглядит примерно так:
do
  x <- xs
  y <- ys
  guard (y > 2)
  return (x * y)

Так что, если вы видите стрелку влево и не похоже, чтобы ожидались сторонние эффекты, — это, вероятно, списочное выражение.

Еще символы


Списки работают так же, как вы в Python: [1, 2, 3] — и в самом деле список из трех элементов. Двоеточие, как в x:xs, означает создание списка с x впереди и xs в конце(cons, для фанатов Lisp.) ++ — конкатенация списков, !! — обращение по индексу. Обратный слэш означает lambda. Если вы видите символ, который не понимаете, попробуйте поискать его в Hoogle (да, он работает с символами!).

Еще шумы


Следующие функции, возможно, шумы, и, возможно, могут быть проигнорированы: liftIO, lift, runX (например, runState), unX (например, unConstructor), fromJust, fmap, const, evaluate, восклицательный знак перед аргументом (f !x), seq, символ решетки (e.g. I# x).

Собирая все вместе


Давайте вернемся к исходному фрагменту кода:
runCommand env cmd state = ...
retrieveState = ...
saveState state = ...

main :: IO ()
main = do
    args <- getArgs
    let (actions, nonOptions, errors) = getOpt Permute options args
    opts <- foldl (>>=) (return startOptions) actions
    when (null nonOptions) $ printHelp >> throw NotEnoughArguments
    command <- fromError $ parseCommand nonOptions
    currentTerm <- getCurrentTerm
    let env = Environment
            { envCurrentTerm = currentTerm
            , envOpts = opts
            }
    saveState =<< runCommand env command =<< retrieveState


С помощью догадок, мы можем получить такой перевод:
def runCommand(env, cmd, state):
   ...
def retrieveState():
   ...
def saveState(state):
   ...

def main():
  args = getArgs()
  (actions, nonOptions, errors) = getOpt(Permute(), options, args)
  opts = **mumble**
  if nonOptions is None:
     printHelp()
     raise NotEnoughArguments
  command = parseCommand(nonOptions)

  currentTerm = getCurrentTerm()
  env = Environment(envCurrentTerm=currentTerm, envOpts=opts)

  state = retrieveState()
  result = runCommand(env, command, state)
  saveState(result)

Недурно для поверхностного понимания синтаксиса Haskell (есть один не переводимый очевидным образом кусок, который требует знания, что такое свертка (вообще-то в Python есть встроенная функция reduce — прим. переводчика). Не весь код на Haskell касается сверток; я повторяю, не беспокойтесь об этом чрезмерно!)
Большинство вещей, которые я назвал «шумами», имеют, на самом деле, имеют за собой очень глубокие причины, и если вам интересно, что стоит за ними, я рекомендую научиться писать на Haskell. Но если вы только читаете, этих правил, я думаю, более чем достаточно.
P.S. Если вам действительно интересно, что делает foldl (>>=) (return startOptions) action: реализует паттерн цепочка обязанностей. Да, черт возьми.

P.P.S. от переводчика: С переводом некоторых терминов мне помог graninas
Tags:
Hubs:
+46
Comments 6
Comments Comments 6

Articles