How do pythonists read Haskell

Original author: Edward Z. Yang
  • Transfer
Have you ever faced the fact that sometimes you need to quickly understand what a piece of code does in a certain unfamiliar language? If the language is similar to what you are used to, you can usually guess the purpose of most of the code - even if you are not very familiar with all the features of the language.
With Haskell, everything is different, since its syntax looks completely different than the syntax of traditional languages. But, in fact, the difference is not so great - you just need to look at the right angle. Here is a quick, for the most part incorrect, and, hopefully, useful guide for interpreting by pythonists (the author uses the word "Pythonista" - approx. Translator ) Haskell code. By the end, you will be able to understand the following piece (part of the code is omitted after ellipses):
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


Ignore all that comes after ::(and ignore the type, class, instance, and newtype). Some swear that types help them understand code. If you are quite new, things like Intand Stringmay help as well such as LayoutClass, and MonadError- no. Do not worry about them.


f a b cbroadcast to f(a, b, c). Haskell omits parentheses and commas. One consequence of this is that sometimes we need parentheses for the arguments: f a (b1 + b2) ctranslates to f(a, b1 + b2, c).

Dollar symbol

Since complex expressions of the form a + bare found quite often, and the Haskelers (the Haskellers author - translator's note ) do not like brackets, the dollar symbol is used to avoid them: it is f $ a + bequivalent f (a + b)and translated в f(a + b). You can consider it a $giant left bracket that automatically closes at the end of the line (and no longer need to write))))), hooray!) In particular, you can nest them, and each will create a nesting level: f $ g x $ h y $ a + b- is equivalent f (g x (h y (a + b)))and translates as f(g(x,h(y,a + b))(although some consider this is bad practice).
Sometimes you can see this option: <$>(with angle brackets). You can consider it the same as $. It also happens <*>- pretend it's a comma, andf <$> a <*> bbroadcast to f(a, b).

Reverse Apostrophes

x `f` ybroadcast to f(x,y). The piece between the apostrophes is a function, usually binary, and the arguments to the right and left.

Equals symbol

Two values ​​are possible. At the beginning of the code block, it means that you simply define a function:
doThisThing a b c = ...
def doThisThing(a, b, c):

Next to the keyword letacts as an assignment operator:
let a = b + c in ...
a = b + c

Left arrow

Also works as an assignment operator:
a <- createEntry x
a = createEntry(x)

Why don't we use the equal sign? Witchcraft. (More precisely, it createEntry xhas side effects. More precisely, it means that the expression is monadic. But this is all sorcery. For now, do not pay attention.)

Right arrow

It's Complicated. We will return to them later.

Keyword do

Noises. Can be ignored. It gives some information - that there will be side effects below, but you will never see the difference in Python.


Noises. Also ignore. (You will never see returnused to control the execution.)


f . g $ a + bbroadcast to f(g(a + b)). In fact, in a Python program, you'll probably see something like the following:
x = g(a + b)
y = f(x)
But Haskell programmers are allergic to extra variables.

Binding Operators and Fish

You can find things like =<<, >>=, <=<and >=>. Simply, these are some more ways to get rid of intermediate variables:
doSomething >>= doSomethingElse >>= finishItUp
x = doSomething()
y = doSomethingElse(x)

Sometimes a Haskell programmer decides that it’s prettier to do it in a different direction, especially if the variable is assigned a value somewhere else:
z <- finishItUp =<< doSomethingElse =<< doSomething
x = doSomething()
y = doSomethingElse(x)
z = finishItUp(y)

The most important thing is to do reverse engineering of what is happening by looking at the definitions doSomething, doSomethingElseand finishItUp: this will give a hint that it is “flowing” past the “fish”. If you do this, you can read <=<, and >=>the same (they are actually carried out the composition functions as .). Read >>as a semicolon (i.e., no assignment):
doSomething >> doSomethingElse

Partial application

Sometimes Haskell programmers call a function, but do not pass enough arguments. Do not be afraid, most likely, they organized the transfer of the remaining arguments elsewhere. Ignore, or look for functions that take anonymous functions as arguments. The usual suspects: map, fold(and its variants) filter, the compositions of the operator ., the operator "Fish» ( =<<, etc). This often happens with numerical operators: (+3)translates to lambda x: x + 3.

Control operators

Rely on instincts: these operators do exactly what you thought! (Even if you think that they should not work like that). So, if you see:, when (x == y) $ doSomething xread how "Since it xis soon equal y, call doSomethingwith an argument x."
Ignore the fact that you cannot actually when(x == y, doSomething(x))cast this in (here doSomething will be called anyway). In fact, it will be more accurate when(x == y, lambda: doSomething x), but it may be more convenient to consider it as whena language construction.
ifand case- keywords. They work as you expect.

Right arrow (for real!)

Right arrows have nothing to do with left arrows. Think of them as colons: they are always somewhere near the keyword caseand the backslash (which the lambda announces: \x -> xtranslates to lambda x: x).
Pattern matching using caseis a pretty nice feature, but it's hard to explain in this post. Perhaps the simplest approximation is a chain if..elif..elsewith variable assignments:
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
  raise Exception("Pattern match failure!")


You can distinguish a wrapping function by what it starts with with. They work like context management in Python:
withFile "foo.txt" ReadMode $ \h -> do
with open("foo.txt", "r") as h:

(You can find out the backslash. Yes, it's a lamb. Yes, it withFile's a function. Yes, you can define your own.)


throw, catch, catches, throwIO, finally, handleAnd all other similar functions work exactly as you expect. This, however, may look funny, because these are all functions, not keywords, with all the consequences. For example:
trySomething x `catch` \(e :: IOException) -> handleError e
catch (trySomething x) (\(e :: IOException) -> handleError e)
except IOError as e:


If you see Nothing, you can think of him as about None. So it isNothing xchecks that x is None. What is the opposite of Nothing? Just. For example, isJust xchecks that x is not None.
You can see a lot of noise related to processing Justand Nonein the right order. One of the most common cases:
maybe someDefault (\x -> ...) mx
if mx is None:
  x = someDefault
  x = mx

Here is a specific option for the case when null is an error:
maybe (error "bad value!") (\x -> ...) x
if x is None:
  raise Exception("bad value!")


Work as expected, however Haskell allows you to create fields without a name:
data NoNames = NoNames Int Int
data WithNames = WithNames {
  firstField :: Int,
  secondField :: Int

Thus, it NoNameswill be represented in Python as a tuple (1, 2), and in the WithNamesfollowing class:
class WithNames:
  def __init__(self, firstField, secondField):
    self.firstField = firstField
    self.secondField = secondField

In this simple way, it NoNames 2 3translates into (2, 3), and WithNames 2 3or WithNames { firstField = 2, secondField = 3 }- into WithNames(2, 3).
Fields are slightly different. The most important thing is to remember that the Haskelers put the names of their fields in front of the variable, while you are most likely used to putting them after. So it is field xbroadcast in x.field. How to record x.field = 2? Well, actually, you cannot do this. Although you can copy:
return $ x { field = 2 }
y = copy(x)
y.field = 2
return y

Or you can create from scratch if you replace x with the name of the data structure (it starts with a capital letter). Why do we allow only copy structures? Because Haskell is a pure language; but do not pay attention to it. Just another Haskell quirk.

List expressions

Initially, they came from the Miranda-Haskell family! They have only a few more characters.
[ x * y | x <- xs, y <- ys, y > 2 ]
[ x * y for x in xs for y in ys if y > 2 ]

It also turns out that the Haskelers often prefer to write list expressions in multi-line form (perhaps they find it easier to read this way). It looks something like this:
  x <- xs
  y <- ys
  guard (y > 2)
  return (x * y)

So if you see a left arrow and it doesn’t seem like third-party effects are expected, this is probably a list expression.

More characters

Lists work just like you do in Python: [1, 2, 3]- and in fact a list of three elements. A colon, as in x:xs, means creating a list with the xfront and xsthe end ( cons, for Lisp fans.) ++- list concatenation, !!- index reference. Backslash means lambda. If you see a character that you don’t understand, try searching in Hoogle (yes, it works with characters!).

More noises

The following functions may noises, and possibly may be ignored: liftIO, lift, runX(e.g. runState), unX(e.g. unConstructor), fromJust, fmap, const, evaluate, exclamation marks before argument ( f !x) seq, the pound sign (eg I# x).

Putting it all together

Let's get back to the original code snippet:
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

Using guesswork, we can get this translation:
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:
     raise NotEnoughArguments
  command = parseCommand(nonOptions)
  currentTerm = getCurrentTerm()
  env = Environment(envCurrentTerm=currentTerm, envOpts=opts)
  state = retrieveState()
  result = runCommand(env, command, state)

It's not bad for a superficial understanding of Haskell syntax (there is one piece that cannot be translated in an obvious way that requires knowledge of what convolution is ( actually, Python has a built-in reduce function - translator comment ). Not all Haskell code deals with convolutions; I repeat, not worry too much about it!)
Most of the things that I called “noise” have, in fact, very deep reasons, and if you are interested in what is behind them, I recommend learning to write in Haskell. But if you are just reading, these rules, I think, are more than enough.
PS If you are really interested in what he is doing foldl (>>=) (return startOptions) action: he implements the chain of duties pattern . Hell yes.

PPS from the translator:With the translation of some terms, graninas helped me

Also popular now: