PythonistsがHaskellを読む方法

特定の馴染みのない言語でコードが何をするのかをすぐに理解する必要があるという事実に出くわしましたか? 言語が慣れ親しんでいる場合、言語のすべての機能に精通していない場合でも、通常はほとんどのコードの目的を推測できます。
Haskellでは、構文が従来の言語の構文とは非常に異なるため、すべてが異なります。 しかし、実際には、違いはそれほど大きくありません-あなたは正しい角度を見る必要があります。 ここに、ほとんどの部分が正しくない、そしてできればpythonistsによる解釈のための便利なガイドがあります著者は「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 


種類


::後のすべてを無視します(また、 typeclassinstance 、およびnewtypeも無視しinstance )。 型がコードを理解するのに役立つと誓う人もいます。 あなたが完全な初心者であれば、 IntStringようなものが役立つかもしれませんが、 LayoutClassMonadErrorようなLayoutClassLayoutClassMonadErrorません。 それらを心配しないでください。

引数


fabcf(a, b, c)変換されます。 Haskellは、括弧とコンマを省略します。 この結果、引数に括弧が必要になる場合があります: fa (b1 + b2) cf(a, b1 + b2, c)変換されます。

ドル記号


a + bという形式の複雑な表現は非常に一般的であり、Haskellers(Haskellersの著者-翻訳者のメモ )は角括弧を好まないため、ドル記号を使用して角括弧を回避しています: f $ a + b f (a + b)と同等であり、変換されます f(a + b)$は行末で自動的に閉じる(そして書く必要はもうありません)))))、巨大な左括弧と考えることができます!)特に、それらをネストでき、それぞれがネストレベルを作成します: f $ gx $ hy $ a + bf (gx (hy (a + b)))と等価であり、 f(g(x,h(y,a + b))変換されます(一部の人はこれを悪い習慣だと考えています)。
次のオプションを見ることができます: <$> (山括弧付き)。 これは$と同じと考えることができます。 また、 <*> -コンマのふりをして、 f <$> a <*> bf(a, b)変換されます。

逆アポストロフィ


x `f` yf(x,y)変換されます。 アポストロフィの間の部分は関数であり、通常はバイナリであり、引数は左右にあります。

等しい記号


2つの値が可能です。 コードブロックの先頭では、単に関数を定義することを意味します。
 doThisThing abc = ... 
==>
 def doThisThing(a, b, c): ... 

キーワードの隣で、割り当て演算子として機能さletます。
 let a = b + c in ... 
==>
 a = b + c ... 


左矢印


代入演算子としても機能します:
 a <- createEntry x 
==>
 a = createEntry(x) 

等号を使用しないのはなぜですか? 魔術。 (より具体的には、 createEntry xは副作用があります。より正確には、式がcreateEntry xであることを意味します。しかし、これはすべて魔術です。今のところ、それを無視してください。)

右矢印


すべてが複雑です。 後でそれらに戻ります。

キーワードを実行


ノイズ。 無視できます。 いくつかの情報が得られます-以下に副作用がありますが、Pythonには違いが見られません。

戻る


ノイズ。 また無視します。 (実行の制御に使用されるreturnは表示されません。)

ポイント


f . g $ a + b f . g $ a + bf(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) 

最も重要なことは、 finishItUpfinishItUp 、およびfinishItUp定義を見ることによって、起こっていることのリバースエンジニアリングを行うことです。これは、「魚」を「流れる」ヒントを与えます。 これを行うと>=>同じ方法で<=<および>=>読み取ることができます(実際、 .などの関数を構成します)。 >>をセミコロンとして読み取り>> (つまり、割り当てなし):
 doSomething >> doSomethingElse 
==>
 doSomething() doSomethingElse() 


部分適用


Haskellプログラマーは関数を呼び出すが、十分な引数を渡さないことがあります。 恐らく、恐らく、彼らは他の場所で残りの議論の転送を組織した。 無視、または引数として匿名関数を受け入れる関数を探します。 一般的な容疑者: mapfold (およびその変形)、 filter 、composition operator . 、魚演算子( =<<など)。 これはしばしば数値演算子で発生します: (+3)lambda x: x + 3変換されます。

制御オペレーター


本能に頼る:これらの演算子はあなたが思ったとおりに動作します! (たとえあなたが彼らがそのように働くべきではないと思うとしても)。 したがって、次のように表示される場合: when (x == y) $ doSomething x 、「 xyに等しいので、引数xを指定してdoSomethingを呼び出す」を読んでください。
実際にこれをwhen(x == y, doSomething(x))に変換できないという事実は無視してください(ここではdoSomethingが呼び出されます)。 実際にはwhen(x == y, lambda: doSomething x)方が正確ですが、言語がwhen構築されるかを考慮when方が便利な場合があります。
ifcaseはキーワードです。 期待どおりに機能します。

右矢印(実際に!)


右矢印は左矢印とは関係ありません。 それらはコロンと考えてください:それらは常にcaseキーワードとバックスラッシュ(ラムダを宣言します: \x -> xlambda x: x変換されます)の近くにあります。
caseを使用したパターンマッチングは非常に便利な機能ですが、この記事では説明が困難です。 おそらく最も簡単な近似は、変数の割り当てを伴うif..elif..elseチェーンです。
 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!") 


ラッピング


で始まるという事実によって、ラッピング関数を区別できます。 Pythonのコンテキスト管理のように機能します。
 withFile "foo.txt" ReadMode $ \h -> do ... 
==>
 with open("foo.txt", "r") as h: ... 

(バックスラッシュを見つけることができます。はい、これはラムです。はい、 withFileは関数です。はい、独自に定義できます。)

例外


throwcatchcatchesthrowIOfinallyhandleおよび他のすべての同様の関数は、期待どおりに機能します。 ただし、これらはすべての機能であり、キーワードではなく、すべての結果を伴うため、これはおかしいように見えるかもしれません。 たとえば、次のとおりです。
 trySomething x `catch` \(e :: IOException) -> handleError e 
===
 catch (trySomething x) (\(e :: IOException) -> handleError e) 
==>
 try: trySomething(x) except IOError as e: handleError(e) 


たぶん


Nothingも表示されNothing場合は、 Noneと考えることができます。 isNothing xは、 x is Noneことをチェックします。 Nothingの反対は何ですか? Just 。 たとえば、 isJust xx is not Noneことを検証します。
JustNone処理に関連する多くのノイズを正しい順序で見ることができます。 最も一般的なケースの1つ:
 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ではtuple (1, 2)として表され、 WithNames次のクラスで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)WithNames(2, 3)ます。
フィールドは少し異なります。 最も重要なことは、Haskelersがフィールドの名前を変数の前に置くことを覚えておくことです。一方、後からフィールドを置くことに慣れていることでしょう。 したがって、 field xx.field変換されx.fieldx.field = 2書き方 まあ、実際には、これを行うことはできません。 コピーできますが:
 return $ x { field = 2 } 
==>
 y = copy(x) y.field = 2 return y 

または、xをデータ構造の名前(大文字で始まる)に置き換えると、ゼロから作成できます。 コピー構造のみを許可するのはなぜですか? Haskellは純粋な言語だからです。 しかし、それに注意を払ってはいけません。 ちょうど別のHaskellの癖。

リスト式


当初、彼らはミランダ・ハスケル家から来ました! 彼らはほんの数文字しか持っていません。
 [ x * y | x <- xs, y <- ys, y > 2 ] 
==>
 [ x * y for x in xs for y in ys if y > 2 ] 

また、Haskelersはリスト式を複数行形式で記述することを好むことが多いことがわかります(おそらく、この方法で読みやすいと思うでしょう)。 次のようになります。
 do x <- xs y <- ys guard (y > 2) return (x * y) 

したがって、左矢印が表示され、サードパーティの効果が期待されるように思われない場合、これはおそらくリスト式です。

他のキャラクター


リストは、Pythonの場合と同じように機能します: [1, 2, 3] -実際には3つの要素のリスト。 x:xsようなコロンは、 xを先頭に、 xsを末尾に持つリストを作成することを意味します(Lispファンの場合はcons++ -リストの連結、 !! -インデックスによる処理。 バックスラッシュはlambda意味します。 理解できない文字が見つかった場合は、 Hoogleで検索してみてください(はい、文字で動作します!)。

より多くのノイズ


次の関数、おそらくノイズであり、おそらく無視される可能性があります: liftIOliftrunXrunX . runState )、 unXunConstructor . unConstructor )、 fromJustfmapconstevaluate 、引数の前の感嘆符( f !x ) 、 seqseq記号(例: 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で書くことを学ぶことをお勧めします。 しかし、読んでいるだけなら、これらのルールは十分すぎると思います。
PS foldl (>>=) (return startOptions) action本当に興味がある場合: 一連の職務パターンを実装します。 はい、地獄。

翻訳者からのPPS: Graninasはいくつかの用語の翻訳を手伝ってくれました

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


All Articles