88

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

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

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

4

3 に答える 3

93

私はそれを半分真実だと考えています。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 クラスを使用して、許可された side-effects を特徴付ける(非常にユニークな) 機能もあります。たとえば、次の関数があるとします。

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"

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

于 2011-07-08T09:59:51.770 に答える
23

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

  • サブルーチンの簡単な作成。値を標準出力と標準エラー出力に出力する関数が必要だとしましょう。サブルーチンを 1 つの短い行で定義すると、次のように記述できます。

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

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

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

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

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

  • コードの再利用が容易。このmapM_関数は頻繁に使用され、他の言語の for-each ループを置き換えます。foreverwhile (true) のように動作するものや、コードを渡してさまざまな方法で実行できるその他のさまざまな関数もあります。そのため、他の言語のループは、Haskell のこれらの制御関数に置き換えられます (これは特別なものではなく、非常に簡単に自分で定義できます)。一般に、これはループ条件を間違えるのを難しくします。for-each ループは、同等の長い反復子 (Java など) や配列インデックス付けループ (C など) よりも間違えにくいのと同じです。

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

    do line <- getLine
       putStrLn (foo line)
    

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

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

他にもメリットはあると思いますが、思いつくのは以上です。

于 2011-07-08T10:01:58.120 に答える
17

他の人がすでに述べたことに加えて、副作用のあるアクションをファーストクラスにすることが役立つ場合があります。アイデアを示すばかげた例を次に示します。

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

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

于 2011-07-08T11:20:28.380 に答える