📜 ⬆️ ⬇️

How do pythonists read Haskell

Have you encountered 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, things are different, because its syntax looks very 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, mostly incorrect, and, I hope, useful guide on how Pythonista interprets Haskell code. By the end, you will be able to understand the following piece (part of the code is omitted after the ellipsis):
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 everything after :: (and also ignore type , class , instance and newtype ). Some swear types help them understand the code. If you are completely new, things like Int and String may help, but LayoutClass like LayoutClass and MonadError not. Do not worry about them.

Arguments


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

Dollar symbol


Since complex expressions like a + b are quite common, and Haskelers do not like brackets, the dollar symbol is used to avoid them: f $ a + b equivalent to f (a + b) and is translated f(a + b) . You can think of $ giant left parenthesis, which automatically closes at the end of the line (and you don’t have to write anymore))))), hurray!) In particular, you can nest them, and each will create a nesting level: f $ gx $ hy $ a + b is equivalent to f (gx (hy (a + b))) and is translated as f(g(x,h(y,a + b)) (although some consider it a bad practice).
Sometimes you can see this option: <$> (with angle brackets). You can consider it the same as $ . <*> Also occurs - pretend that it is a comma, and f <$> a <*> b translated to f(a, b) .

Reverse apostrophes


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

Equals symbol


Two values ​​are possible. At the beginning of a block of code, it means that you simply define a function:
 doThisThing abc = ... 
==>
 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


It also works as an assignment operator:
 a <- createEntry x 
==>
 a = createEntry(x) 

Why don't we use the equal sign? Witch. (More precisely, createEntry x has side effects. More precisely, it means that the expression is monadic. But this is all a witch. Don't pay attention to it yet.)

Right arrow


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

Keyword do


Noises You can ignore. 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 the return used to control execution.)

Point


f . g $ a + b f . g $ a + b translated to f(g(a + b)) . In fact, in a Python program, you are more likely to see something like the following:
 x = g(a + b) y = f(x) 
But Haskell programmers are allergic to unnecessary variables.

Operators and tying fish


You can meet 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 is more beautiful to do it in a different direction, especially if somewhere in the variable a value is assigned to:
 z <- finishItUp =<< doSomethingElse =<< doSomething 
==>
 x = doSomething() y = doSomethingElse(x) z = finishItUp(y) 

The most important thing is to do the reverse engineering of what is happening, looking at the definitions of doSomething , doSomethingElse and finishItUp : this will give a hint that it “flows” by “fish”. If you do this, you can read <=< and >=> same way (in fact, they perform the composition of functions, like . ). Read >> like 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 accept anonymous functions as arguments. Common suspects: map , fold (and its variants), filter , composition operator . , fish operator ( =<< , etc). This often happens with numeric operators: (+3) translated to lambda x: x + 3 .

Control operators


Rely on instincts: these operators do exactly what you think! (Even if you think they should not work like this). So, if you see: when (x == y) $ doSomething x , read as "Soon x is equal to y , call doSomething with argument x ".
Ignore the fact that you cannot actually translate this in when(x == y, doSomething(x)) (here doSomething will be called anyway). Actually, it will be more accurate when(x == y, lambda: doSomething x) , but it may be more convenient to consider when construction of a language.
if and case are keywords. They work the way you expect.

Right Arrow (for real!)


The arrows to the right have nothing to do with the arrows to the left. Think of them as colons: they are always somewhere near the case keyword and the backslash (which declares lambda: \x -> x translated into 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 the if..elif..else chain with variable assignments:
 case moose of Foo xyz -> 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 the machining function by the fact that 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 learn backslash. Yes, it is lamba. Yes, withFile is a function. Yes, you can define your own.)

Exceptions


throw , catch , catches , throwIO , finally , handle and all other similar functions work exactly as you would expect. This, however, can look funny, because these are all functions, not keywords, with all that it implies. 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 it as None . So isNothing x checks that x is None . What is the opposite of Nothing? Just . For example, isJust x verifies that x is not None .
You can see a lot of noise associated with Just and None processing 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!") 


Records


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

Thus, NoNames will be represented in Python with a tuple (1, 2) , and WithNames will be represented by the following class:
 class WithNames: def __init__(self, firstField, secondField): self.firstField = firstField self.secondField = secondField 

In this simple way, NoNames 2 3 translated to (2, 3) , and WithNames 2 3 or WithNames { firstField = 2, secondField = 3 } to WithNames(2, 3) .
The fields are somewhat different. The most important thing is to remember that the Haskelers put the names of their fields in front of the variable, whereas you are most likely accustomed to putting them after. So field x translates to x.field . How to write x.field = 2 ? Well, actually, you can't do that. Although you can copy:
 return $ x { field = 2 } 
==>
 y = copy(x) y.field = 2 return y 

Or you can create it from scratch by replacing x with the name of the data structure (it starts with a capital letter). Why do we only allow copying structures? Because Haskell is a pure language; but ignore it. Just another haskell fad.

List expressions


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

Also, it turns out that Haskelers often prefer to write list expressions in a multi-line form (perhaps, they think that this is easier to read). It looks like this:
 do x <- xs y <- ys guard (y > 2) return (x * y) 

So, if you see a left arrow and it does not seem like side effects are expected, this is probably a list expression.

More characters


Lists work just like you in Python: [1, 2, 3] - and in fact a list of three elements. A colon, as in x:xs , means creating a list with x in front and xs at the end ( cons , for Lisp fans.) ++ - concatenation of lists, !! - appeal by index. Reverse slash means lambda . If you see a symbol that you don’t understand, try searching it in Hoogle (yes, it works with symbols!).

Still noises


The following functions may be noise, and may be ignored: liftIO , lift , runX (for example, runState ), unX (for example, unConstructor ), fromJust , fmap , const , evaluate , exclamation mark before the argument ( f !x ) , seq , lattice character (eg I# x ).

Putting it all together


Let's go back to the source 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 


With the help of 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 shallow understanding of Haskell syntax (there is one obviously not translatable piece that requires knowledge of what convolution is ( actually, Python has a built-in function of reduce - approx. Translator ). Not all Haskell code deals with convolutions; I repeat, not worry about it too much!)
Most of the things that I called “noises” have, in fact, very deep reasons behind them, and if you are wondering what is behind them, I recommend learning to write in Haskell. But if you only read, these rules, I think, are more than enough.
PS If you really wonder what foldl (>>=) (return startOptions) action : implements the chain of duty pattern. Hell yes.

PPS from translator: Graninas helped me with the translation of some terms

Source: https://habr.com/ru/post/132969/


All Articles