web-dev-qa-db-ja.com

Haskellが(時々)「Best Imperative Language」と呼ばれるのはなぜですか?

(この質問がトピックに合っていることを望みます-回答を検索しようとしましたが、明確な回答が見つかりませんでした。これがトピックから外れているか既に回答済みの場合は、モデレート/削除してください。)

Haskellがbest imperative languageであるという冗談のコメントを何度か聞いた/読んだことを覚えているが、Haskellは通常functional特徴。

だから私の質問は、Haskellのどのような性質/機能(もしあれば)が、Haskellが最良の命令型言語とみなされることを正当化する理由を与えるか、それとも実際には冗談ですか?

81
hvr

私はそれを半分の真実だと思います。 Haskellには抽象化する驚くべき能力があり、それには命令型アイデアの抽象化が含まれます。たとえば、Haskellには命令型のwhileループは組み込まれていませんが、それを記述するだけで、次のようになります。

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

このレベルの抽象化は、多くの命令型言語にとって困難です。これは、クロージャーを持つ命令型言語で実行できます。例えば。 Python and C#。

しかし、Haskellには、Monadクラスを使用して許可された副作用を特徴づける(非常にユニークな)機能もあります。たとえば、関数がある場合:

foo :: (MonadWriter [String] m) => m Int

これは「命令型」関数になりますが、次の2つのことしかできないことがわかっています。

  • 文字列のストリームを「出力」する
  • intを返します

コンソールに出力したり、ネットワーク接続を確立したりすることはできません。抽象化機能と組み合わせて、「ストリームを生成する計算」などに作用する関数を作成できます。

Haskellの抽象化能力が、すべてを非常に優れた命令型言語にしています。

ただし、誤った半分は構文です。 Haskellは非常に冗長で扱いにくい命令形式で使用すると思います。リンクリストの最後の要素を見つける上記のwhileループを使用した命令型の計算例を次に示します。

lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
    lst <- newIORef xs
    ret <- newIORef (head xs)
    while (not . null <$> readIORef lst) $ do
        (x:xs) <- readIORef lst
        writeIORef lst xs
        writeIORef ret x
    readIORef ret

IORefガベージ、二重読み取り、読み取りの結果をバインドする必要があるすべて(<$>)インライン計算の結果を操作するには...すべてが非常に複雑に見えます。 functionalの観点からは非常に理にかなっていますが、命令型言語はこれらの詳細の大部分を敷き詰めて使いやすくする傾向があります。

確かに、おそらく別のwhileスタイルのコンビネータを使用すると、よりクリーンになります。しかし、その哲学を十分に理解すると(豊富な組み合わせを使用して自分自身を明確に表現する)、関数型プログラミングに再び到達します。命令型のHaskellは、適切に設計された命令型言語のように「フロー」しません。 python。

結論として、構文的なフェイスリフトを使用すると、Haskellが最高の命令型言語になる可能性があります。しかし、フェイスリフトの性質上、内部的に美しくリアルなものを、外部的に美しい偽物に置き換えることになります。

[〜#〜] edit [〜#〜]:コントラストlastEltとこれpython音訳:

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

行数は同じですが、各行のノイズはかなり少なくなっています。


EDIT 2

Haskellでのpureの置き換えは次のようになります。

lastElt = return . last

それでおしまい。または、Prelude.lastの使用を禁止している場合:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

または、任意の Foldable データ構造で動作させ、実際にはエラーではないことを認識した場合needIO

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

Mapを使用して、たとえば:

λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"

(.)演算子は 関数構成 です。

87
luqui

それは冗談ではなく、私はそれを信じています。 Haskellを知らない人のために、これをアクセスしやすいものにしようと思います。 Haskellは、(特に)do記法を使用して命令型コードを記述できるようにします(はい、モナドを使用しますが、心配する必要はありません)。 Haskellが提供する利点のいくつかを以下に示します。

  • サブルーチンの簡単な作成。関数に値をstdoutとstderrに出力させたいとしましょう。サブルーチンを1行で定義して、次のように書くことができます。

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • コードを簡単に渡すことができます。上記を書いたので、printBoth関数を使用してすべての文字列のリストを出力したい場合、サブルーチンをmapM_関数に渡すことで簡単に実行できます。

    mapM_ printBoth ["Hello", "World!"]
    

    別の例は、必須ではありませんが、並べ替えです。長さだけで文字列をソートするとします。あなたは書ける:

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    

    これにより、["b"、 "cc"、 "aaaa"]が得られます。 (それよりも短く書くこともできますが、今のところ気にしないでください。)

  • コードを簡単に再利用できます。そのmapM_関数は頻繁に使用され、他の言語のfor-eachループを置き換えます。しばらく(true)のように振る舞うforeverや、コードを渡してさまざまな方法で実行できるさまざまな関数もあります。そのため、他の言語のループはHaskellのこれらの制御関数に置き換えられます(特別なものではありません。自分で簡単に定義できます)。一般的に、これはfor-eachループが同等のロングハンドイテレーター(Javaの場合)や配列インデックスループ(Cの場合など)よりも間違いを起こしにくいように、ループ条件の間違いを起こしにくくします。

  • 割り当てではなくバインディング。基本的に、変数に割り当てることができるのは一度だけです(単一の静的割り当てのように)。これにより、任意のポイントでの変数の可能な値に関する多くの混乱がなくなります(その値は1行でのみ設定されます)。
  • 含まれる副作用。 stdinから行を読み取り、関数を適用してからstdoutに書き込みたいとしましょう(fooと呼びます)。あなたは書ける:

    do line <- getLine
       putStrLn (foo line)
    

    fooには予期しない副作用(グローバル変数の更新、メモリの割り当て解除など)がないことがすぐにわかります。これは、型がString-> Stringである必要があるためです。つまり、純粋な関数;どんな値を渡しても、副作用なしで毎回同じ結果を返さなければなりません。 Haskellは、副作用コードを純粋なコードからうまく分離します。 CやJavaのようなものでは、これは明らかではありません(getFoo()メソッドは状態を変更しますか?.

  • ガベージコレクション。最近では多くの言語がガベージコレクションされていますが、言及する価値があります。メモリの割り当てと割り当て解除の面倒はありません。

おそらく他にもいくつかの利点がありますが、それらは思い浮かぶものです。

22
Neil Brown

他の人がすでに言及していることに加えて、副作用のあるアクションをファーストクラスにすると便利な場合があります。ここにアイデアを示す愚かな例があります:

f = sequence_ (reverse [print 1, print 2, print 3])

この例は、実際に実行する前に、副作用を伴う計算(この例ではprint)を構築し、データ構造に入れたり、他の方法で操作したりする方法を示しています。

16
tibbe