Сталкивались ли вы с тем, что иногда надо быстро понять, что делает кусок кода на неком незнакомом языке? Если язык похож на то, к чему вы привыкли, как правило, можно догадаться о назначении большей части кода — даже если вы не очень хорошо знакомы со всеми фичами языка.
С 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 вы скорее увидите примерно следующее:Но у Haskell-программистов аллергия на лишние переменные.x = g(a + b) y = f(x)
Операторы связывания и рыбка
Можно встретить вещи вроде
=<<, >>=, <=< и >=>. Попросту, это еще несколько способов избавиться от промежуточных переменных:==>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
