4

I wrote this code:

toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                   outputFile <- openFile "couples.txt" WriteMode
                   readAndChange inputFile outputFile

readAndChange i o = do iseof <- hIsEOF i
                       if iseof then (return o)
                       else do line <- hGetLine i
                               hPutStrLn o (show (extractNameAndId line))
                               readAndChange i o

I wonder if I can rewrite this code using just one function, using something similar to this pattern:

function x = do ...
                label
                .....
                if ... then label else exit
4

5 に答える 5

13

不必要に命令的な方法でプログラミングすることで、生活を困難にしています。あなたは美しい Haskell 言語でプログラミングしていて、goto構文を探しています!

だけでなくimport Control.Applicative (<$>)、書いてみませんか

readAndChange' = writeFile "couples.txt" =<< 
    unlines.map (show.extractNameAndId).lines <$> readFile "deletedId.csv" 

(そうです、これはほぼワンライナーです。クリーンで機能的なスタイルであり、行の読み取りと書き込みのメカニズムによって整理されています。処理は可能な限り純粋なコードで行われ、入力と出力のみが IO ベースです。)

説明:

ここでunlines.map (show.extractNameAndId).linesは、入力を行に切り刻み、 を使用してそれぞれに適用しextractNameAndId、で再度結合して処理します。showmapunlines

unlines.map (show.extractNameAndId).lines <$> readFile "deletedId.csv"ファイルを読み取り、処理関数を適用します。<$>のための楽しい構文ですfmap

writeFile "couples.txt" =<< getanswerと同じですgetanswer >>= writeFile "couples.txt"- 上記のように答えを得て、それをファイルに書き込みます。

書いてみてgreet xs = "hello " ++ xsから、ghciでこれらを楽しみましょう

greet "Jane"        -- apply your pure function purely
greet $ "Jane"      -- apply it purely again
greet <$> ["Jane","Craig","Brian"]  -- apply your function on something that produces three names
greet <$> Just "Jane"               -- apply your function on something that might have a name
greet <$> Nothing                   -- apply your function on something that might have a name
greet <$> getLine                   -- apply your function to whatever you type in 
greet <$> readFile "deletedId.csv"  -- apply your function to your file 

最後の 1 つは、 での使用方法<$>ですreadAndChange。deletedId.csv に大量のデータがある場合、hello を見逃すことになりますが、もちろんできます。

greet <$> readFile "deletedId.csv" >>= writeFile "hi.txt"
take 4.lines <$> readFile "hi.txt"

最初の 4 行を表示します。

したがって$、指定した引数で関数を使用できます。greet :: String -> Stringしたがって、 と書く場合greet $ personpersonは 型Stringである必要がありますが、 と書く場合greet <$> someone、は文字列のリスト、 、をsomeone生成するものであれば何でもかまいません。技術的には ですが、最初に型クラスと Applicative Functor を読む必要があります。Learn You a Haskell for Great Good は優れたリソースです。StringIO StringMaybe Stringsomeone :: Applicative f => f String

さらに面白いことに、複数の引数を持つ関数がある場合でも、素敵な Applicative スタイルを使用できます。

insult :: String -> String -> String
insult a b = a ++ ", you're almost as ugly as " ++ b

試す

insult "Fred" "Barney"
insult "Fred" $ "Barney"
insult <$> ["Fred","Barney"] <*> ["Wilma","Betty"]
insult <$> Just "Fred" <*> Nothing
insult <$> Just "Fred" <*> Just "Wilma"
insult <$> readFile "someone.txt" <*> readFile "someoneElse.txt"

ここでは<$>、関数の後、<*>必要な引数の間に使用します。それがどのように機能するかは、最初は少し戸惑いますが、効果的な計算を記述する最も機能的なスタイルです。

次に Applicative Functor について読んでください。彼らは素晴らしいです。
http://learnyouahaskell.com/functors-applicative-functors-and-monoids
http://en.wikibooks.org/wiki/Haskell/Applicative_Functors

于 2012-09-11T20:20:57.507 に答える
3

パターンを使用して再帰を実行できますがlet、これは再帰関数を個別に定義するのと似ています。

main = do 
    let x = 10 
    let loop = do 
        print 1 
        when (x<20) loop 
    loop

fixfromを使用しControl.Monad.Fixて同様の動作を実現することもできます

main = do 
    let x = 10 
    fix $ \loop -> do 
        print 1 
        when (x<20) loop

あなたが言っているのは一種のgoto labelパターンです。そのような動作を実現できるかどうかはわかりませんが、fixorを使用するとlet、再帰を簡単に実現できます。

Cont[編集]モナドを次のように使用するなど、同様の結果を達成するためのパターンがいくつかあります。

getCC' :: MonadCont m => a -> m (a, a -> m b)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))

main = (`runContT` return) $ do 
    (x, loopBack) <- getCC' 0
    lift (print x)
    when (x < 10) (loopBack (x + 1))
    lift (putStrLn "finish")

から数字を出力し1 to 10ます。より良い説明については、継続を使用した goto を参照してください。

Gotoモナドとトランスフォーマーもあります。私はそれを使用していません。あなたのニーズに適しているかもしれません。

于 2012-09-11T19:48:57.137 に答える
3
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Either

readAndChange i o = do
    result <- fmap (either id id) $ runEitherT $ forever $ do
        iseof <- lift $ hIsEof i
        when iseof $ left o -- Break and return 'o'
        line <- lift $ hGetLine i
        lift $ hPutStrLn o $ show $ extractNameAndId line
    -- 'result' now contains the value of 'o' you ended on
    doSomeWithingWith result

この手法が機能する理由を理解するには、この をお読みください

于 2012-09-11T19:38:40.407 に答える
2

最初に行うべきことは、Control.Monadモジュールのドキュメントを読むことです。これは、Haskell コードを書く上で絶対に不可欠です。Control.Monad.Loopsその間に、Hackage からパッケージをインストールし、そのドキュメントを読んでください。そこの機能に特に興味があるかもしれませんwhileM_

import Data.Functor ((<$>)
import Control.Monad.Loops (whileM_)

readAndChange i o = 
    whileM_ notEOF $ do line <- hGetLine i
                        hPutStrLn o (show (extractNameAndId line))
        where notEOF = not <$> (hIsEOF i)

問題のライブラリはwhileM_次のように実装されています。これは、探しているパターンです。

-- |Execute an action repeatedly as long as the given boolean expression
-- returns True.  The condition is evaluated before the loop body.
-- Discards results.
whileM_ :: (Monad m) => m Bool -> m a -> m ()
whileM_ p f = do
        x <- p
        if x
                then do
                        f
                        whileM_ p f
                else return ()

それでも、あなたがこれを過度に命令的な方法で書いていることには同意せざるを得ません。このように考えてみてください: プログラムは基本的に入力文字列を出力文字列に変換しています。これは、プログラムのロジックのコアが次の型を持つ必要があることをすぐに示唆しています。

transformInput :: String -> String
transformInput = ...

変換は行単位で進行します。これは、この方法でスケッチを改良できることを意味します (lines関数は文字列を線に分割しunlines、リストを再結合します)。

transformInput :: String -> String
transformInput input = unlines (map transformLine (lines input))

transformLine :: String -> String
transformLine line = show (extractNameAndId line)

これで、関数内のロジックのコアが得transformInputられたので、入力ハンドルと出力ハンドルに接続する必要があります。stdin と stdout を扱っている場合は、interact関数を使用してそれを行うことができます。しかし、実際にはその実装を盗んで変更することができます。

hInteract       ::  Handle -> Handle -> (String -> String) -> IO ()
hInteract i o f =   do s <- hGetContents i
                       hPutStr o (f s)

そして今、ほら:

toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                   outputFile <- openFile "couples.txt" WriteMode
                   hInteract inputFile outputFile transformInput

警告: すべてのコードはテストされていません。


最後にもう 1 つ、完全な開示のために: ここでのトリックは、遅延 I/OhGetContentsを実行することです。これにより、基本的に、ハンドルの内容全体を として扱うことができるため、ハンドルの内容に関数を適用できます。しかし、それはすべて遅延して行われるため、実際にはファイル全体を一度に読み取る必要はありません。StringtransformInput

これは最も簡単な方法であり、習得する必要がありますが、大きな弱点があります。つまり、ハンドルが閉じているときに制御できなくなる可能性があるということです。ただし、迅速で汚いプログラムの場合、これは問題ありません。

于 2012-09-11T23:12:15.177 に答える
0

命令型プログラミング言語とは異なり、また他の関数型プログラミング言語とは異なり、Haskellには、forループまたはwhileループを記述するための構文構造が含まれていません。これは、ここで求めているようです。

再帰プロセスと反復プロセスは、再帰関数によって均一にキャプチャできるという考え方です。反復プロセスが特定の種類の再帰関数としてキャプチャされるだけです。これらの関数は末尾再帰です。do-block内に表示されるような命令型コードも例外ではありません。ループごとに新しい関数を定義する必要があるため、明示的なループ構造が不足していることに気付くかもしれません。したがって、ある意味でループに名前を付ける必要があります。ただし、これは、次の3つの理由から、Haskellアプローチの均一性と単純さを支払うのに取るに足らない代償です。

  1. トップレベルでループを表す関数を定義する必要はありません。ローカルで定義できます。

  2. Haskellでは、多くの人が通常、これらの種類のループに常に同じ名前を使用します。goここで人気のある選択肢はとですaux。したがって、コードは次のように書き直すことができます。

    toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                       outputFile <- openFile "couples.txt" WriteMode
                       let go = do
                            iseof <- hIsEOF inputFile
                            if iseof then (return outputFile)
                            else do line <- hGetLine inputFile
                                    hPutStrLn outputFile (show (extractNameAndId line))
                                    go
                       go
    
  3. 最後に、ループを作成する必要がない場合が多いため、ループ構造がないことは非常に重要ではありません。あなたの場合、このスレッドの他の回答は、これを行うための多くの方法を示しています。

于 2012-09-11T20:56:04.270 に答える