Python を Haskell に「音訳」する簡単な方法があります。これは、モナド変換子を巧みに使用することで実現できますが、これは恐ろしく聞こえるかもしれませんが、実際にはそうではありません。Haskell では、変更可能な状態 (たとえば、append
andpop
操作が変更を実行している) や例外などの効果を使用する場合、純粋性のために、もう少し明示的にする必要があります。上から始めましょう。
parse :: String -> SchemeExpr
parse s = readFrom (tokenize s)
Python docstring には「文字列からスキーム式を読み取る」と書かれていたので、これを自由に型シグネチャ ( String -> SchemeExpr
) としてエンコードしました。型が同じ情報を伝えるため、その docstring は時代遅れになります。さて... とは何ですかSchemeExpr
? コードによると、スキーム式は、int、float、symbol、またはスキーム式のリストにすることができます。これらのオプションを表すデータ型を作成しましょう。
data SchemeExpr
= SInt Int
| SFloat Float
| SSymbol String
| SList [SchemeExpr]
deriving (Eq, Show)
Int
扱っている を として扱う必要があることをHaskell に伝えるにはSchemeExpr
、 でタグ付けする必要がありSInt
ます。他の可能性についても同様です。に移りましょうtokenize
。
tokenize :: String -> [Token]
再び、docstring は型シグネチャに変わります: aを s のString
リストに変えToken
ます。さて、トークンとは何ですか?コードを見ると、左右の括弧文字が明らかに特別なトークンであり、特定の動作を示していることがわかります。それ以外は... 特別ではありません。括弧を他のトークンとより明確に区別するためにデータ型を作成することもできますが、元の Python コードにもう少し近づけるために、文字列を使用してみましょう。
type Token = String
では、書いてみましょうtokenize
。最初に、関数チェーンを Python に少し似たものにする簡単な小さな演算子を書きましょう。Haskell では、独自の演算子を定義できます。
(|>) :: a -> (a -> b) -> b
x |> f = f x
tokenize s = s |> replace "(" " ( "
|> replace ")" " ) "
|> words
words
のHaskell版ですsplit
。ただし、Haskell にはreplace
、私が知っている調理済みのバージョンはありません。トリックを行う必要があるものは次のとおりです。
-- add imports to top of file
import Data.List.Split (splitOn)
import Data.List (intercalate)
replace :: String -> String -> String -> String
replace old new s = s |> splitOn old
|> intercalate new
splitOn
とのドキュメントを読めばintercalate
、この単純なアルゴリズムは完全に理にかなっているはずです。Haskellers は通常、これを と書きますが、 Python の読者が理解しやすいようにここでreplace old new = intercalate new . splitOn old
使用しました。|>
replace
は 3 つの引数を取ることに注意してください。Haskell では、任意の関数を部分的に適用できます。これは非常に優れています。|>
型の安全性が高いことを除けば、UNIX パイプのように機能します。
まだ私と一緒に?にスキップしましょうatom
。ネストされたロジックは少し見にくいので、少し異なる方法でクリーンアップしてみましょう。このタイプを使用してEither
、より優れたプレゼンテーションを行います。
atom :: Token -> SchemeExpr
atom s = Left s |> tryReadInto SInt
|> tryReadInto SFloat
|> orElse (SSymbol s)
Haskell には自動強制変換関数int
andfloat
がないため、代わりに をビルドしtryReadInto
ます。仕組みは次のとおりですEither
。値をスレッド化します。Either
値は aまたはLeft
aRight
です。従来、Left
エラーまたは失敗を通知するために使用され、Right
成功または完了を通知します。Haskell では、Python 風の関数呼び出しチェーンをシミュレートするには、「self」引数を最後の引数として配置するだけです。
tryReadInto :: Read a => (a -> b) -> Either String b -> Either String b
tryReadInto f (Right x) = Right x
tryReadInto f (Left s) = case readMay s of
Just x -> Right (f x)
Nothing -> Left s
orElse :: a -> Either err a -> a
orElse a (Left _) = a
orElse _ (Right a) = a
tryReadInto
文字列を解析しようとしている型を決定するために、型推論に依存しています。解析が失敗した場合、そのLeft
位置に同じ文字列を再生成するだけです。成功した場合は、必要な機能を実行し、結果をそのRight
位置に配置します。前の計算が失敗した場合に備えて、値をorElse
指定することで を排除できます。ここで例外の代わりとしてEither
どのように機能するかがわかりますか? Python コード内の s はEither
常に関数自体の内部でキャッチされるため、決して例外が発生しないことがわかっています。同様に、Haskell コードでは、関数の内部で使用していますが、公開するインターフェイスは純粋です。ValueException
atom
Either
Token -> SchemeExpr
、目に見える副作用はありません。
では、に移りましょうread_from
。まず、自問自答してください: この関数にはどのような副作用がありますか? tokens
を介して引数を変更しpop
、 という名前のリストに内部変更を加えていL
ます。また、SyntaxError
例外が発生します。この時点で、ほとんどの Haskeller は「ああ、いや、副作用だ!ひどい!」と言って手を上げます。しかし、実際のところ、Haskeller は常に副作用も使用します。人々を怖がらせて成功を避けるために、私たちはそれらを「モナド」と呼んでいます。変異はモナドで実現できState
、例外はモナドで実現できますEither
(驚き!)。両方を同時に使用したいので、実際には「モナド変換子」を使用しますが、これについては後で説明します。そうじゃないクラフトを過ぎて見ることを学ぶと、怖いです。
まず、いくつかのユーティリティ。これらは単純な配管操作です。raise
Python のように「例外を発生」させ、PythonwhileM
のように while ループを記述させます。後者の場合、効果が発生する順序を明示する必要があります。最初に効果を実行して条件を計算し、それが の場合True
は本体の効果を実行し、再度ループします。
import Control.Monad.Trans.State
import Control.Monad.Trans.Class (lift)
raise = lift . Left
whileM :: Monad m => m Bool -> m () -> m ()
whileM mb m = do
b <- mb
if b
then m >> whileM mb m
else return ()
ここでも、純粋なインターフェースを公開したいと考えています。ただし、 a が存在する可能性があるSyntaxError
ため、型シグネチャで結果がaまたは a のいずれかになることを示します。これは、Java でメソッドが発生させる例外に注釈を付ける方法を連想させます。SyntaxError が発生する可能性があるため、の型シグネチャも変更する必要があることに注意してください。SchemeExpr
SyntaxError
parse
data SyntaxError = SyntaxError String
deriving (Show)
parse :: String -> Either SyntaxError SchemeExpr
readFrom :: [Token] -> Either SyntaxError SchemeExpr
readFrom = evalStateT readFrom'
渡されたトークン リストに対してステートフルな計算を実行します。ただし、Python とは異なり、呼び出し元に失礼なことをしたり、渡されたリスト自体を変更したりすることはありません。代わりに、独自の状態空間を確立し、与えられたトークン リストに初期化します。構文糖衣を提供する記法を使用do
して、命令的にプログラミングしているように見せます。StateT
モナド変換子は、get
、put
、およびmodify
状態操作を提供します。
readFrom' :: StateT [Token] (Either SyntaxError) SchemeExpr
readFrom' = do
tokens <- get
case tokens of
[] -> raise (SyntaxError "unexpected EOF while reading")
(token:tokens') -> do
put tokens' -- here we overwrite the state with the "rest" of the tokens
case token of
"(" -> (SList . reverse) `fmap` execStateT readWithList []
")" -> raise (SyntaxError "unexpected close paren")
_ -> return (atom token)
readWithList
型シグネチャを見てもらいたいので、この部分を別のコードの塊に分割しました。コードのこの部分は新しい scopeStateT
を導入するので、以前のモナド スタックの上に別のスコープを単純に重ねます。ここで、、、get
およびput
操作modify
はL
、Python コードで呼び出されるものを参照します。でこれらの操作を実行したい場合は、モナド スタックの 1 つのレイヤーを取り除くためにtokens
、操作の前に を付けるだけです。lift
readWithList :: StateT [SchemeExpr] (StateT [Token] (Either SyntaxError)) ()
readWithList = do
whileM ((\toks -> toks !! 0 /= ")") `fmap` lift get) $ do
innerExpr <- lift readFrom'
modify (innerExpr:)
lift $ modify (drop 1) -- this seems to be missing from the Python
Haskell では、リストの最後に追加するのは効率が悪いので、代わりに先頭に追加し、後でリストを逆にしました。パフォーマンスに関心がある場合は、より優れたリストのようなデータ構造を使用できます。
完全なファイルは次のとおりです: http://hpaste.org/77852
したがって、Haskell を初めて使用する場合、これはおそらく恐ろしく見えるでしょう。私のアドバイスは、少し時間を与えることです。モナドの抽象化は、人々が思っているほど恐ろしいものではありません。ほとんどの言語に組み込まれているもの (ミューテーション、例外など) は、代わりに Haskell がライブラリを介して提供することを学ぶ必要があります。Haskell では、必要な効果を明示的に指定する必要があり、それらの効果を制御することは少し不便です。しかしその代わりに、Haskell はより多くの安全性を提供するため、間違った効果を誤って混同することはありません。また、効果を組み合わせてリファクタリングする方法を完全に制御できるため、より強力になります。