1

コマンド ラインで 2 つの整数を受け取り、それらを使って何か面白いことを行うプログラムを作成しようとしています。整数の読み取り/解析は、比較的単純なコードである必要があるため、できるだけ簡単かつ命令的に記述したいと考えています。

私が直面している問題は、Haskell ではエラー処理がそれほど単純ではないということです。Haskellではパターンマッチングがよく使われているようです。これにより、命令型バージョンよりもコードを理解するのが少し難しくなっているようです。

プログラムは次のように実行されます (この例では、2 つの数値を加算するだけです)。

$ ./my_prog
ERROR: Must run like `./my_prog NUM_A NUM_B`.
$ ./my_prog cat 1
ERROR: Could not parse NUM_A "cat" as integer
$ ./my_prog 10 dog
ERROR: Could not parse NUM_B "dog" as integer
$ ./my_prog 10 1
11

命令型疑似 Python でやりたいことは次のとおりです。

function die (errorMessage):
    print("ERROR: %s" % errorMessage)
    sys.exit(1)

function main ():
    if len(sys.argv) != 2:
        die("Must run program like `%s NUM_A NUM_B`" % sys.progname)

    num_a = 0
    num_b = 0

    try:
        num_a = int(sys.argv[0])
    except:
        die("Could not parse NUM_A \"%s\" as integer" % sys.argv[0])

    try:
        num_b = int(sys.argv[1])
    except:
        die("Could not parse NUM_B \"%s\" as integer" % sys.argv[1])

     doSomethingInteresting(num_a, num_b)

function doSomethingInteresting (num_a, num_b):
    print(num_a + num_b)

Python では、基本的に main 関数を上から下に読むことができ、すべてのエラー処理は簡単です。 複数のパターン マッチングを行わずに、Haskell でこのシンプルで直接的なエラー処理を実装する方法はありますか?

これは私が思いついた Haskell コードで、これと同じタスクを実行しますが、パターン マッチング セクションが複数あるため、Python コードよりもはるかに複雑に見えます。

module Main ( main
            )
            where

import System.Environment (getArgs, getProgName)
import System.Exit (ExitCode(..), exitWith)
import Text.Read (readMaybe)

die :: String -> IO a
die err = do putStrLn $ "ERROR: " ++ err
             exitWith (ExitFailure 1)

main :: IO ()
main = do
        args <- getArgs
        progName <- getProgName
        case args of
            [strNumA, strNumB] -> do
                let maybeNumA = readMaybe strNumA :: Maybe Int
                    maybeNumB = readMaybe strNumB :: Maybe Int
                checkMaybeArgs strNumA maybeNumA strNumB maybeNumB
            _ -> die ("Must run like `" ++ progName ++ " NUM_A NUM_B`.")
    where
        checkMaybeArgs :: String -> Maybe Int -> String -> Maybe Int -> IO ()
        checkMaybeArgs badStrNumA Nothing _ _ =
            die ("Could not parse NUM_A \"" ++ badStrNumA ++ "\" as integer")
        checkMaybeArgs _ _ badStrNumB Nothing =
            die ("Could not parse NUM_B \"" ++ badStrNumB ++ "\" as integer")
        checkMaybeArgs _ (Just numA) _ (Just numB) = doSomethingInteresting numA numB

        doSomethingInteresting :: Int -> Int -> IO ()
        doSomethingInteresting numA numB = print $ numA + numB

(また、私の Haskell スタイルに他に何か問題がある場合は、修正していただければ幸いです。)

編集: 最近、Haskell で例外を処理するさまざまな方法について説明しているブログ記事を見つけました。それは多少関連しています:

http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors

4

4 に答える 4

6

これを書く方法は次のとおりです(外部ライブラリを使用せずに)

import Text.Read
import Text.Printf
import System.Environment
import Control.Monad
import System.Exit


parsingFailure :: String -> String -> IO a
parsingFailure name val = printf "ERROR: Couldn't parse %s : %s as an integer\n" 
                                  name val >> exitWith (ExitFailure 1)

validateArgs :: [String] -> IO (Integer, Integer)
validateArgs [a, b] = liftM2 (,) (parse "VAL A" a) (parse "VAL B" b)
  where parse name s = maybe (parsingFailure name s) return $ readMaybe s
validateArgs _      = putStrLn "Wrong number of args" >> exitWith (ExitFailure 1)

main :: IO ()
main = do
  (a, b) <- getArgs >>= validateArgs
  print $ a + b

もちろん興味深いのはvalidateArgs. 最初に 1 つのパターン マッチを行いますが、それ以降はmaybeコンビネータを使用してパターン マッチングを適切に抽象化します。これにより、はるかにクリーンなコード IMO が得られます。maybeコンビネータは、デフォルト値 bと継続a -> bを取り、 に展開しMaybe aますb。この場合、デフォルト値は解析の失敗であり、継続はモナドに注入aされます。IO

于 2013-11-27T05:07:07.250 に答える
4

@jozefgs ソリューションは簡潔で気に入っていますが、exitWith. 確かに、このような小さな例では問題なく動作しますが、一般的に、そのように「途中で」プログラムを終了するのは悪い考えだと思います。これは確かに、私が Python バージョンの嫌いな特性でもあります。いずれにせよ、私の解決策は次のとおりです。その一部を以下で説明します。

import System.Environment (getProgName, getArgs)
import Text.Read (readMaybe)

doSomethingInteresting :: Int -> Int -> IO ()
doSomethingInteresting a b = print (a + b)

readWith :: Read a => (String -> e) -> String -> Either e a
readWith err str = maybe (Left $ err str) return $ readMaybe str


main = do
  progName <- getProgName
  args     <- getArgs

  either putStrLn id $ do
    (a,b) <- case args of
               [a,b] -> return (a,b)
               _     -> Left ("Must run program like `" ++ progName ++ " NUM_A NUM_B`")

    num_a <- readWith (\s -> "Could not parse NUM_A \"" ++ s ++ "\" as integer") a
    num_b <- readWith (\s -> "Could not parse NUM_B \"" ++ s ++ "\" as integer") b

    return $ doSomethingInteresting num_a num_b

これにはパターン マッチングがあることは知っていますが、それは私が望むものを表現する最もクリーンな方法です。リストに 2 つの要素がある場合は、リストから除外します。そうでない場合は、エラーメッセージが必要です。それをうまく表現する方法はありません。

しかし、ここで強調したいことが他にもいくつかあります。

単一の出口点

まず、すでに述べたように、プログラムにはいくつかの「終了点」がありません。つまり、何かの途中でプログラムを終了することはありません。main関数がメイン関数ではなく、より深いところにあり、いくつかのファイルを開いているか、上記の関数で何かを開いていると想像してください。ファイルなどを閉じるには、適切な方法を使用する必要があります。そのため、プログラムを終了するのではなく、プログラムを実行させ、数値がない場合は計算を行わないようにしています。

プログラムを終了することは、めったに良い考えではありません。多くの場合、保存したいもの、閉じたいファイル、復元したい状態などがあります。

Eitherエラーに注釈を付ける

初心者にはわかりにくいかもしれませんが、readWith基本的にはread何かを試して、成功すれば戻ってきますRight <read value>。失敗した場合は を返しLeft <error message>ます。エラー メッセージは、読み取ろうとした文字列によって異なります。

そこで、このプログラムでは、

λ> readWith errorMessage "15"
Right 15

その間

λ> readWith errorMessage "crocodile"
Left "Cannot read \"crocodile\" as an integer, dummy!"

Either値を転送したいが、途中でエラーが発生する可能性があり、エラーの対処方法がわかるまでエラーメッセージを保持したい場合、型は優れています。コミュニティのコンセンサスはRight、「正しい」値をLeft示し、何らかのエラーが発生したことを示す必要があるというものです。

Either値は、一種のより制御された (読み取り: より良い) 例外システムです。

do構文はあなたの友達です

このdo構文は、エラーを処理するのに非常に便利です。計算の結果がLeft値になるとすぐに、Haskell はdoブロックから抜け出し、Left. doこれは、この場合、内部ブロック全体が次のような結果になることを意味します

Right (print 41)

または何かのような

Left "Could not parse NUM_A \"crocodile\" as integer"

関数はeither、エラー メッセージを出力するかIO、値であるアクションを返すようにしRightます。アクションを値として格納できるというのは Python から来ると奇妙に思えるかもしれませんがprint、Haskell ではそれが普通です。I/O アクションは、Haskellの第一級市民であると言います。言い換えれば、それらを渡してデータ構造に保存し、いつ実行するかを自分で決めることができます。

于 2013-11-27T09:35:15.343 に答える
1

これが私のやり方です:

module Main where

import Text.Read (readMaybe)
import System.Environment (getProgName, getArgs)

main = getArgs >>= \argv -> case argv of
  [x, y] -> case (readMaybe x, readMaybe y) of
    (Nothing, _      ) -> error $
      "ERROR: Could not parse NUM_A " ++ show x ++ " as integer"
    (_      , Nothing) -> error $
      "ERROR: Could not parse NUM_B " ++ show y ++ " as integer"
    (Just a , Just b ) -> print $ a + b
  _ -> do
    pname <- getProgName
    error $ "ERROR: Must run like `" ++ pname ++ " NUM_A NUM_B`."

タプルのパターン マッチを一度に複数の式で一致させる方法に注意してください。これにより、複数の埋め込みのケース式が必要なくなります。

于 2013-11-27T05:26:34.503 に答える