haskell-jp / beginners #23 at 2023-05-01 23:25:28 +0900

分岐処理について教えて下さい

Maybe を使った場合
import Control.Monad

f :: String -> Maybe String
f x = do
    guard (x == "MZ")

    return "SUCCESS"


main :: IO ()
main = do
    print $ f "MZ"
    print $ f "ELF"

このような形 (guard) で条件によって処理を中断ができます。
しかし、それ以外の文脈の場合この方法は使えないと思います。

具体的には以下のようなプログラムを作っています。
import 
import System.Directory ( getCurrentDirectory )
import System.FilePath ( joinPath )

import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Internal as BI ( w2c )
import Data.Binary.Get

getC :: Get Char
getC = BI.w2c <$> getWord8

readPE :: Get (Maybe String)
readPE = do
    isMZ <- (== "MZ") <$> sequence [getC, getC]

    if isMZ then
        return $ Just "SUCCESS"
    else
        return Nothing

main :: IO ()
main = do
    xpath <- joinPath . flip (:) ["src", "a.exe"] <$> getCurrentDirectory

    withBinaryFile xpath ReadMode $ \h -> do
        dat <- B.hGetContents h
        print $ runGet readPE dat

    print "done."

上記の readPE 関数の中で if による分岐を行っていますが
もし、この後も条件による分岐が必要だった場合 if 文をネスト
させていくような形で作るのが正しいのでしょうか ?
もっといい書き方があるかもしれませんが、とりあえず guard を使った書き方をしたい場合、 MaybeT モナド変換子 を使えば実現できそうです。
早々にありがとうございました m(_ _)m

モナド変換子はこのような使い方をするのですね。
すぐには理解できるとは思いませんが、MaybeT から勉強してみます
単にifのネストを避けたいということなら、ガード付きケースまたはMultiWayIfでいけると思いますが。。。
readPE = do
    cs <- sequence (replicate 2 getC)
    case cs of
        _ | "MZ" == cs -> return $ Just "SUCCEESS"
          | otherwise  -> return $ Nothing

ご回答ありがとうございます
上記だけの分岐であればガードでも良いのかと思ったのですが

(haskell の書き方ではないですが)
function readPE(a, b, ...) {
  if (a == 100) {
    if (b == 200) {
      if ...

    } else {
      return Nothing
    }
  } else {
    return Nothing
  }
}

上記のように複数の条件による if が続いた場合
階層が深くなっていってしまう様な書き方が一般的なのか
それとも、他にスマートな方法があるのかと考えていました。
そこで、Maybe の guard のように、条件に合致したら関数自体は
Nothing で戻るようにできれば
function readPE(a, b, ...) {
  guard (a != 100)
  guard (b != 200)

  return "SUCCESS"
}

のように書けるのかと思ったのですが、これが Get (Maybe String) だったので
guard が Maybe ではなく Get のそれになってしまっているのか、
意図した通りに動かないな。といった次第でした

すみません、うまく説明もできていないです orz
全ての条件がなりたつときと、それ以外を分けているだけのように見えますが、そうではないですか?
あるいは、1つでも条件がなりたつ場合と、それ以外、1つもなりたたない場合を分けている?
申し訳ありません。実はまだそこまでは考えていない状況です m(_ _)m

プログラムを作成中に if による分岐が発生したので
これは、今後も条件が発生するたびに if else のネストを書かなければ
ならないのかな。他に書き方があるものかな。と思い質問させていただいた状況です。

もう少し整理してから、改めて質問させていただきたいと思います。
ああ、条件がなりたったら、do ブロックから抜けたいというこですか?
はい。
foo() {
  if (a == 100) {
    return ERROR
  }

  if (b == 200) {
    return ERROR
  }

  return SUCCESS
}

C 言語っぽく書くと、こんな感じで途中で抜けられたら
と思っていました。

つたない説明から意図までくんでもらってありがとうございます。
if-then-elseのネストで、構文上フラットに書くだけでいいなら^^; (ここまでするならMaybeTがいいかなぁ)
do { statement1
   ; iF cond1 (return r1) $ do
   { statement2
   ; iF cond2 (return r2) $ do
   { statement3
   ; iF cond3 (return r3) $ do
   { statement4
   ; iF otherwise (return r4) undefined
   }}}}
iF c t f = if c then t else f
長々とお付き合いいただきありがとうございます。

やりたいのは確かにフラットに書きたいということですが
おっしゃる通りだと思いますので、MaybeT を勉強してみます。
もし、よろしければ、書こうたされているプログラムの仕様を具体的に教えていただけると嬉しいです。どうしても命令的に書きたくなる例というのを収集して、無理矢理、関数的に書いてみるという試みをやっています。
プログラムが書ける。というほど haskell がわかっているわけでもなく
とはいえ、アウトプットしないと書けるようにもならないのだろうな。
と思って、手始めに Windows の PE フォーマットをダンプしてみよう

と考えて以下の資料を参考に
http://hp.vector.co.jp/authors/VA050396/tech_06.html
(上記にも載せさせていただいたように) ファイルの最初の 2 バイトを取得して
"MZ" と比較したところで、今回の質問をさせていただきました。

import 
import System.Directory ( getCurrentDirectory )
import System.FilePath ( joinPath )

import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Internal as BI ( w2c )
import Data.Binary.Get

getC :: Get Char
getC = BI.w2c <$> getWord8

readPE :: Get (Maybe String)
readPE = do
    isMZ <- (== "MZ") <$> sequence [getC, getC]

    if isMZ then
        return $ Just "SUCCESS"
    else
        return Nothing

main :: IO ()
main = do
    xpath <- joinPath . flip (:) ["src", "a.exe"] <$> getCurrentDirectory

    withBinaryFile xpath ReadMode $ \h -> do
        dat <- B.hGetContents h
        print $ runGet readPE dat

    print "done."

ですので、この後もファイルヘッダの項目をチェックして不正な値を検出したときは
readPE 関数が Nothing を戻すように考えていました。

具体的には (if をネストして考えた場合)
readPE :: Get (Maybe String)
readPE = do
    isMZ <- (== "MZ") <$> sequence [getC, getC]

    if isMZ then
        -- 適当に追加しています -->
        e_cblp <- getWord16le
        if e_cblp > 0 then
            e_cp <- getWord16le
            if e_cp > 0 then
                ...
                return $ Just "SUCCESS"
            else
                return Nothing
        else
            return Nothing
        -- 適当に追加しています <--
    else
        return Nothing

全て正常であれば、例えば
data ImageHeader = ImageHeader { magic :: String, ... }

のような型に入れて Just ImageHeader として返却する。

といったようなことを考えていました。
ありがとうございます。お聞かせていただいた範囲では、インタラクションは単純ですので、スピードを気にしなければ、入力文字列 -> 出力文字列があればよいので、関数的に書けそうですね。
ごめんなさい。
そのように回答をいただきましたが、それもピンときていないくらいです orz

つたないもので恐縮ですが、何かの参考になるのであれば嬉しいです
あまり気になさらないでください。大富豪的というか仙人的に考えると(空間計算量を無視すると)全部よみこんでしまってから、パーズして、プリティプリントして、出力文字列を全部構成してしまってから、出力するのであれば、IOが絡む部分は最初の読み込みと、最後の書き出しの部分のみなので、それ以外は純粋な関数として書けるでしょう。パーズもプリティプリントもデータ変換するだけなので、制御フローを気にせず書けるとうことです。
お気遣いいただきありがとうございます。
確かに最初に全て読み込んでしまう方法なら対象がシンプルになりそうな気がします。

気持ち的には Persec 的な魔法の何かでバイナリ処理ができるかな。とかも
考えましたが、そもそも Persec 的な魔法の使い方もよくわかっていないのです orz
パーザコンビネータライブラリは精緻なのでそのまま読んで理解するのは難しいかもしれません。「 プログラミングHaskell第2版」 などが読み易いと思います。かなり古い教科書ですが、 の 1.6 A parser for the Core language に基本的なアイデア(モナドという言葉はでてきませんが)が書かれいてなるほどと思えるかもしれません。
親切にありがとうございます

山下さんの本は一通り入手済で、まずは「ふつうの」から読み始め
それ以上は今の自分には難しすぎるな。と認識している最中です orz
でも、「ふつうの」本を読んだおかげで、haskell をやってみようかな
と思えました。
それも重ねてありがとうございました。