Сталкивались ли вы с тем, что иногда надо быстро понять, что делает кусок кода на неком незнакомом языке? Если язык похож на то, к чему вы привыкли, как правило, можно догадаться о назначении большей части кода — даже если вы не очень хорошо знакомы со всеми фичами языка.
С Haskell все по-другому, так как его синтаксис выглядит совсем иначе, нежели синтаксис традиционных языков. Но, на самом деле, разница не так велика — нужно просто взглянуть под правильным углом. Здесь приводится быстрое, по большей части некорректное, и, надеюсь, полезное руководство по интерпретации питонистами (автор использует слово «Pythonista» — прим. переводчика) кода на Haskell. К концу вы будете способны понять следующий кусок (часть кода опущена за троеточиями):
Игнорируйте все, что стоит после
Так как сложные выражения вида
Иногда можно увидеть такой вариант:
Возможны два значения. В начале блока кода он означает, что вы просто определяете функцию:
Рядом с ключевым словом
Тоже работает как оператор присваивания:
Почему мы не используем знак равенства? Колдунство. (Если точнее,
Все сложно. Вернемся к ним позже.
Ключевое слово
Шумы. Можно игнорировать. Оно дает некоторую информацию, — что ниже будут побочные эффекты, но вы никогда не увидите разницы в Python.
Шумы. Также игнорировать. (Вы никогда не увидите
Можно встретить вещи вроде
Иногда Haskell-программист решает, что красивее сделать это в другом направлении, особенно, если где-то там переменной присваивается значение:
Самое важное — сделать реверс-инжиниринг происходящего, посмотрев на определения
Иногда Haskell-программисты вызывают функцию, но не передают достаточно аргументов. Не бойтесь, скорее всего, они организовали передачу остальных аргументов в другом месте. Игнорируйте, или ищите функции, которые принимают анонимные функции как аргументы. Обычные подозреваемые:
Положитесь на инстинкты: эти операторы делают именно, то что вы подумали! (Даже если вы думаете, что они не должны работать так). Так что, если вы видите:
Игнорируйте тот факт, что вы не можете на самом деле транслировать это в
У стрелок вправо нет ничего общего со стрелками влево. Думайте о них, как о двоеточиях: они всегда где-то рядом с ключевым словом
Pattern matching с использованием
Вы можете отличить обрачивающую функцию по тому, что она начинается с
(Вы можете узнать обратный слэш. Да, это лямба. Да,
Если вы видите
Можно увидеть много шумов, связанных с обработкой
Вот специфический вариант для случая, когда null — это ошибка:
Работают, как и ожидается, однако Haskell позволяет вам создавать поля без имени:
Таким образом,
Таким простым образом,
Поля несколько отличаются. Самое главное — запомнить, что хаскелеры ставят имена своих полей перед переменной, тогда как вы, скорее всего, привыкли ставить их после. Так что
Или можно создать с нуля, если заменить x именем структуры данных (она начинается с заглавной буквы). Почему мы позволяем только копировать структуры? Потому что Haskell — это чистый язык; но не обращайте на это внимания. Просто еще одна причуда Haskell.
Изначально они пришли из семейства Miranda-Haskell! В них только немного больше символов.
Также, оказывается, что хаскелеры часто предпочитают писать списочные выражения в многострочной форме (возможно, они считают, что так их проще читать). Это выглядит примерно так:
Так что, если вы видите стрелку влево и не похоже, чтобы ожидались сторонние эффекты, — это, вероятно, списочное выражение.
Списки работают так же, как вы в Python:
Следующие функции, возможно, шумы, и, возможно, могут быть проигнорированы:
Давайте вернемся к исходному фрагменту кода:
С помощью догадок, мы можем получить такой перевод:
Недурно для поверхностного понимания синтаксиса Haskell (есть один не переводимый очевидным образом кусок, который требует знания, что такое свертка (вообще-то в Python есть встроенная функция reduce — прим. переводчика). Не весь код на Haskell касается сверток; я повторяю, не беспокойтесь об этом чрезмерно!)
Большинство вещей, которые я назвал «шумами», имеют, на самом деле, имеют за собой очень глубокие причины, и если вам интересно, что стоит за ними, я рекомендую научиться писать на Haskell. Но если вы только читаете, этих правил, я думаю, более чем достаточно.
P.S. Если вам действительно интересно, что делает
P.P.S. от переводчика: С переводом некоторых терминов мне помог graninas
С 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