How do pythonists read Haskell
- 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):
Ignore all that comes after
Since complex expressions of the form
Sometimes you can see this option:
Two values are possible. At the beginning of the code block, it means that you simply define a function:
Next to the keyword
Also works as an assignment operator:
Why don't we use the equal sign? Witchcraft. (More precisely, it
It's Complicated. We will return to them later.
Keyword
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
You can find things like
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:
The most important thing is to do reverse engineering of what is happening by looking at the definitions
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:
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:,
Ignore the fact that you cannot actually
Right arrows have nothing to do with left arrows. Think of them as colons: they are always somewhere near the keyword
Pattern matching using
You can distinguish a wrapping function by what it starts with
(You can find out the backslash. Yes, it's a lamb. Yes, it
If you see
You can see a lot of noise related to processing
Here is a specific option for the case when null is an error:
Work as expected, however Haskell allows you to create fields without a name:
Thus, it
In this simple way, it
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
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.
Initially, they came from the Miranda-Haskell family! They have only a few more characters.
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:
So if you see a left arrow and it doesn’t seem like third-party effects are expected, this is probably a list expression.
Lists work just like you do in Python:
The following functions may noises, and possibly may be ignored:
Let's get back to the original code snippet:
Using guesswork, we can get this translation:
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
PPS from the translator:With the translation of some terms, graninas helped me
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
Types
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 Int
and String
may help as well such as LayoutClass
, and MonadError
- no. Do not worry about them.Arguments
f a b c
broadcast 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) c
translates to f(a, b1 + b2, c)
.Dollar symbol
Since complex expressions of the form
a + b
are 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 + b
equivalent 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 <*> b
broadcast to f(a, b)
.Reverse Apostrophes
x `f` y
broadcast 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
let
acts 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 x
has 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.
Return
Noises. Also ignore. (You will never see
return
used to control the execution.)Point
f . g $ a + b
broadcast 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)
finishItUp(y)
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
, doSomethingElse
and 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
==>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 x
read how "Since it x
is soon equal y
, call doSomething
with 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 when
a language construction. if
and 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
case
and the backslash (which the lambda announces: \x -> x
translates to lambda x: x
). Pattern matching using
case
is a pretty nice feature, but it's hard to explain in this post. Perhaps the simplest approximation is a chain if..elif..else
with 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
else:
raise Exception("Pattern match failure!")
Wrapping
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.)Exceptions
throw
, catch
, catches
, throwIO
, finally
, handle
And 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)
==>try:
trySomething(x)
except IOError as e:
handleError(e)
Maybe
If you see
Nothing
, you can think of him as about None
. So it isNothing x
checks that x is None
. What is the opposite of Nothing? Just
. For example, isJust x
checks that x is not None
. You can see a lot of noise related to processing
Just
and None
in the right order. One of the most common cases:maybe someDefault (\x -> ...) mx
==>if mx is None:
x = someDefault
else:
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!")
Posts
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
NoNames
will be represented in Python as a tuple (1, 2)
, and in the WithNames
following class:class WithNames:
def __init__(self, firstField, secondField):
self.firstField = firstField
self.secondField = secondField
In this simple way, it
NoNames 2 3
translates into (2, 3)
, and WithNames 2 3
or 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 x
broadcast 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:
do
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 x
front and xs
the 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:
printHelp()
raise NotEnoughArguments
command = parseCommand(nonOptions)
currentTerm = getCurrentTerm()
env = Environment(envCurrentTerm=currentTerm, envOpts=opts)
state = retrieveState()
result = runCommand(env, command, state)
saveState(result)
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