Haskellが(時々)「最高の命令型言語」と呼ばれるのはなぜですか?


83

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

Haskellが最高の命令型言語であるという冗談半分のコメントを何度か聞いたり読んだりしたことを覚えています。もちろん、Haskellはその機能機能で最もよく知られているため、奇妙に聞こえます。

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


3
donsbot.wordpress.com/2007/03/10/…<-プログラム可能なセミコロン。
ビビアン2011

10
この引用はおそらく、厄介な分隊へ取り組みの紹介の終わりに由来します「要するに、Haskellは世界で最も優れた命令型プログラミング言語です」と言うHaskellのモナド入出力、並行性、例外、および外国語呼び出し
ラッセルオコナー

@ラッセル:そのことわざの最も可能性の高い起源(見たところSPJ自身である)を指摘してくれてありがとう!
hvr 2011

Haskellで厳密な命令型OOを実行できます:ekmett / structs
Janus Troelsen

回答:


90

私はそれを半分の真実だと思います。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とC#。

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

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

これは「命令型」の関数になる可能性がありますが、実行できるのは2つのことだけです。

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

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

Haskellを非常に優れた命令型言語にするのは、実際には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ガベージ、二重読み取り、読み取りの結果をバインドする必要<$>がある、インライン計算の結果を操作するためのfmapping()...それはすべて非常に複雑な外観です。機能的な観点からは非常に理にかなっていますが、命令型言語は、使いやすくするために、これらの詳細のほとんどを敷物の下で一掃する傾向があります。

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

結論として、構文の改良により、Haskellはおそらく最高の命令型言語かもしれません。しかし、フェイスリフトの性質上、内部的に美しく本物の何かを外部的に美しく偽物の何かに置き換えることになるでしょう。

編集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 

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


編集2

価値があるのは、Haskellの純粋な代替品がどのように見えるかです:

lastElt = return . last

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

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

または、任意のFoldableデータ構造で機能させ、実際にエラーを処理する必要 がないことを認識したい場合は、次のようにしますIO

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"

(.)オペレータは、関数組成物


2
抽象化を増やすことで、IORefノイズの煩わしさを大幅に軽減できます。
2011

1
@augustss、うーん、私はそれについて興味があります。より多くのドメインレベルの抽象化を意味しますか、それとも単に命令型サブ言語を構築することによってですか?」前者については同意しますが、私の心は命令型プログラミングを低抽象化と関連付けています(私の作業仮説は、抽象化が増えるにつれて、スタイル。私は考えることができないため、機能上の収束)が、後者については、私は本当に、あなたが何を意味するか見て興味があると思いますか私の頭の上から。
luqui

2
@luqui STの使用は、許容される副作用を特徴付ける良い例です。ボーナスとして、STから純粋な計算に戻ることができます。
fuz 2011

4
比較としてPythonを使用することは、完全に公平ではありません。あなたが言うように、Pythonはうまく設計されており、私が精通している構文上最もクリーンな命令型言語の1つです。同じ比較では、ほとんどの命令型言語は命令型スタイルで使用するのが厄介であると主張します...しかし、おそらくそれはまさにあなたが意図したことです。;]
CA McCann 2011

5
後世のための会話の脚注:@augustssはIORefアドホック多相性を使用してsを暗黙的にするか、少なくともGHCへの変更を試みて妨害されます。:[
CA McCann 2011

22

それは冗談ではありません、そして私はそれを信じます。Haskellを知らない人のためにこれにアクセスできるようにします。Haskellは(とりわけ)do-notationを使用して、命令型コードを記述できるようにします(はい、モナドを使用しますが、心配する必要はありません)。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ループを置き換えます。またforever、while(true)のように動作する関数や、コードを渡してさまざまな方法で実行できるさまざまな関数もあります。したがって、他の言語のループは、Haskellのこれらの制御関数に置き換えられます(これは特別なことではありません。自分で簡単に定義できます)。一般に、これにより、for-eachループが、同等の長いイテレータ(Javaなど)や配列インデックスループ(Cなど)よりも間違っているのと同じように、ループ条件を間違えることが難しくなります。

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

    do line <- getLine
       putStrLn (foo line)
    

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

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

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


9
強力な型安全性を追加します。Haskellは、コンパイラが大きなクラスのエラーを排除することを可能にします。最近いくつかのJavaコードに取り組んだ後、nullポインターがどれほどひどいのか、合計タイプなしでOOPがどれだけ欠落しているかを思い出しました。
Michael Snoyman 2011

1
精巧にありがとう!あなたが言及した利点は、Haskellが「命令型」効果をファーストクラスのオブジェクト(したがって組み合わせ可能)として扱い、それらの効果を区切られた範囲に「封じ込める」機能に要約されているようです。これは適切な圧縮された要約ですか?
hvr 2011

19
@Michael Snoyman:しかし、OOPでは合計タイプは簡単です!データ型のチャーチエンコーディングを表す抽象クラス、ケースのサブクラス、各ケースを処理できるクラスのインターフェイスを定義し、制御フローのサブタイプポリモフィズムを使用して、各インターフェイスをサポートするオブジェクトを合計オブジェクトに渡すだけです(あなたがすべき)。これ以上簡単なことはありません。なぜデザインパターンが嫌いなのですか?
CA McCann 2011

9
@camccann冗談を言っていることは知っていますが、それは基本的に私がプロジェクトで実装したものです。
Michael Snoyman 2011

9
@マイケル・スノイマン:それなら良い選択です!本当の冗談は、冗談のように聞こえる方法で、ほとんど最高のエンコーディングを説明していたことです。ハ、ハ!...絞首台にすべての方法を笑う
CAマッキャン

16

他の人がすでに述べたことに加えて、副作用のある行動を一流にすることは時々有用です。これはアイデアを示すためのばかげた例です:

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

この例はprint、実際に実行する前に、副作用のある計算を構築し(この例では)、データ構造に配置するか、他の方法で操作する方法を示しています。


これに対応するJavaScriptコードは次のようになると思いますcall = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse())。私が見る主な違いは、いくつかの追加が必要なこと() =>です。
Hjulle
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.