Haskellでレンズを使用して命令的にプログラミングする

Haskellには、変更や状態を操作するための組み込みツールがないため、多くの世間知らずのレビューが寄せられています。 したがって、ステートフルアップルパイを焼く場合は、最初にステートを操作するための演算子の全体を作成する必要があります。 しかし、これはすでに興味を持って支払われており、すでに完成した段階であり、Haskellプログラマーは、自己記述型の命令型言語で見つけることができるものよりも、よりエレガントで簡潔で強力な命令型コードを楽しんでいます。 しかし、あなた自身がこれを確信するでしょう。



レンズ





エレガントなコードへのチケットは、レンズライブラリです。

通常どおりデータを定義し、フィールド名の先頭にアンダースコアのみを追加します。 たとえば、ゲームを次のように定義できます。

 data Game = Game { _score :: Int , _units :: [Unit] , _boss :: Unit } deriving (Show)
      
      





クリーチャーでいっぱい(ユニット):

 data Unit = Unit { _health :: Int , _position :: Point } deriving (Show)
      
      





その位置はポイントによって決定されます:

 data Point = Point { _x :: Double , _y :: Double } deriving (Show)
      
      





直接使用しないため、フィールドにアンダースコアを追加します。 代わりに、私たちはそれらを使って、より快適に作業できるレンズを構築します。



レンズは2つの方法で構築できます。 最初のオプションは、 Control.Lens



便利なlens



機能を使用してレンズを手動で作成することです。 たとえば、次のように_score



フィールドのscore



レンズを定義できます。

 import Control.Lens score :: Lens' Game Int score = lens _score (\game v -> game { _score = v })
      
      





複雑なデータタイプをナビゲートするためのマップとしてのLens



タイプ。 _score



Game



から_score



するためにscore



レンズを使用し_score





タイプは、開始位置と終了方法を反映します_score



Lens' Game Int



は、 Game



で開始し、 Int



を終了する必要があることを意味します(この場合、 _score



フィールドの場合)。 同様に、当社の他のレンズは、そのタイプの開始点と終了点を明確に反映しています。

 units :: Lens' Game [Unit] units = lens _units (\game v -> game { _units = v }) boss :: Lens' Game Unit boss = lens _boss (\game v -> game { _boss = v }) health :: Lens' Unit Int health = lens _health (\unit v -> unit { _health = v }) position :: Lens' Unit Point position = lens _position (\unit v -> unit { _position = v }) x :: Lens' Point Double x = lens _x (\point v -> point { _x = v }) y :: Lens' Point Double y = lens _y (\point v -> point { _y = v })
      
      





しかし、私たちはしばしば怠け者であり、ルーチンコードを書きたくないのですが、この場合、 Template Haskell



を使用して別のパスを選択し、レンズを作成できます。

 {-# LANGUAGE TemplateHaskell #-} import Control.Lens data Game = Game { _score :: Int , _units :: [Unit] , _boss :: Unit } deriving (Show) data Unit = Unit { _health :: Int , _position :: Point } deriving (Show) data Point = Point { _x :: Double , _y :: Double } deriving (Show) makeLenses ''Game makeLenses ''Unit makeLenses ''Point
      
      





覚えておいてください、パターン化されたHaselはmakeLenses宣言をデータ型宣言の後にmakeLenses



にトランペットします。



初期状態


次に行う必要があるのは、ゲームの初期状態を初期化することです。

initialState ::ゲーム
 initialState :: Game initialState = Game { _score = 0 , _units = [ Unit { _health = 10 , _position = Point { _x = 3.5, _y = 7.0 } } , Unit { _health = 15 , _position = Point { _x = 1.0, _y = 1.0 } } , Unit { _health = 8 , _position = Point { _x = 0.0, _y = 2.1 } } ] , _boss = Unit { _health = 100 , _position = Point { _x = 0.0, _y = 0.0 } } }
      
      





ダンジョンのボスと戦う3人のヒーローを作成しました。 戦いを始めましょう!



最初のステップ


これでレンズを使用できます! 戦士がボスを攻撃するための関数を作成しましょう。

 import Control.Monad.Trans.Class import Control.Monad.Trans.State strike :: StateT Game IO () strike = do lift $ putStrLn "*shink*" boss.health -= 10
      
      





strike



機能はコンソールに同様の音を出力し、ボス10のヘルスユニットのヘルスを低下させます。

攻撃関数のタイプは、 StateT Game IO



モナドで操作していることを示しています。 これは、副作用(つまりIO



)の上に純粋なゲームの状態(つまりStateT Game



)のレイヤーを作成し、コンソールでの戦闘からの最重要効果を同時に印刷できるようにする組み込み言語だと思うかもしれません。 ここで覚えておく必要があるのは、副作用を使用する場合はlift



機能を使用する必要があるということだけです。

インタプリタで関数を使用してみましょう( ghci



)。 これを行うには、初期状態が必要です。

execStateTストライクinitialState
 >>> execStateT strike initialState *shink* Game {_score = 0, _units = [Unit {_health = 10, _position = Poin t {_x = 3.5, _y = 7.0}},Unit {_health = 15, _position = Point {_ x = 1.0, _y = 1.0}},Unit {_health = 8, _position = Point {_x = 0 .0, _y = 2.1}}], _boss = Unit {_health = 90, _position = Point { _x = 0.0, _y = 0.0}}}
      
      





execStateT



関数は、状態コードと初期状態を取得して開始し、新しい状態を生成します。 インタプリタが自動的に画面に表示し、結果をすぐに分析できます。 結果は混乱でしたが、目を鍛えると、ボスのヘルスポイントが90になっていることがわかります。

結果の状態に対して最初に新しい変数を作成すると、これをより簡単に確認できます。

 >>> newState <- execStateT strike initialState *shink*
      
      





その後、必要な情報を抽出します。

 >>> newState^.boss.health 90
      
      







構成





次のコードは、命令型のオブジェクト指向コードに非常に似ています。

 boss.health -= 10
      
      





ここで何が起こっているのですか?? Haskellは間違いなくマルチパラダイム言語ではありませんが、マルチパラダイムコードに現れるものがあります。

信じられないほど、このコードには言語に組み込まれた機能はありません!



待って、 (.)



機能的な構成ですか?! ほんと?

これは、すべてのレンズマジックが発生する場所です。 レンズは最も一般的な機能であり、「マルチパラダイム」コードはすべて、機能の混合物にすぎません!



実際、 Lens' ab



タイプは、高階関数のタイプの同義語です。

 type Lens' ab = forall f . (Functor f) => (b -> fb) -> (a -> fa)
      
      





今すべてを理解する必要はありません。 Lens' ab



は、入力引数として型(b -> fb)



を取り、型(b -> fb)



(a -> fa)



新しい関数を返す高次関数であることを思い出してください。 Functor



は理論の一部であり、現在では「魔法」とFunctor



ことができます。

そのボスを確認してください。 健康:: Lens 'Game Int
この知識を武器に、 boss



health



機能のタイプをどのように拡張できるか見てみましょう。

 boss :: Lens' Game Unit --   : boss :: (Functor f) => (Unit -> f Unit) -> (Game -> f Game) health :: Lens' Unit Int --   : health :: (Functor f) => (Int -> f Int) -> (Unit -> f Unit)
      
      





次に、機能構成の定義を見てみましょう。

 (.) :: (b -> c) -> (a -> b) -> (a -> c) (f . g) x = f (gx)
      
      





型変数を次のように置き換えた場合に注意してください。

 a ~ (Int -> f Int) b ~ (Unit -> f Unit) c ~ (Game -> f Game)
      
      





次に、2つのレンズの構成に対して完全に一意の対応を取得します。

 (.) :: ((Unit -> f Unit) -> (Game -> f Game)) -> ((Int -> f Int ) -> (Unit -> f Unit)) -> ((Int -> f Int ) -> (Game -> f Game))
      
      





Lens'



と同義語を逆にすると、次のようになります。

 (.) :: Lens' Game Unit -> Lens' Unit Int -> Lens' Game Int boss . health :: Lens' Game Int
      
      





レンズ構成もレンズであることになります! 実際、レンズはカテゴリーを形成します。ここで、 (.)



はカテゴリー構成演算子であり、恒等関数id



もレンズです。

 (.) :: Lens' xy -> Lens' yz -> Lens' xz id :: Lens' xx
      
      





その結果、演算子の近くのスペースを削除できるという事実を使用して、オブジェクト指向コードの表記法のようなコードを取得します!



カテゴリを使用すると、コンポーネントをその場で簡単に接続およびグループ化できます。 たとえば、上司の健康状態を頻繁に変更すると予想される場合、レンズの構成を決定できます。

 bossHP :: Lens' Game Int bossHP = boss.health
      
      





これで、以前はboss.health



が必要だった場所boss.health



どこでも使用できます。

 strike :: StateT Game IO () strike = do lift $ putStrLn "*shink*" bossHP -= 10
      
      





また、健康の新しい意味を見つけます。

 >>> newState^.bossHP 90
      
      







列挙可能


レンズは非常にエレガントな理論に基づいているため、ほとんどの命令型言語では簡単にできないことを実現できます。



たとえば、私たちの上司が火を吐いてヒーローにダメージを与えるドラゴンだとしましょう。 レンズを使用すると、この効果を1行で実現できます。

 fireBreath :: StateT Game IO () fireBreath = do lift $ putStrLn "*rawr*" units.traversed.health -= 3
      
      





これにより、新しい方法でレンズを操作できるようになります!

 traversed :: Traversal' [a] a
      
      





traversed



は、リスト全体を手動でトラバースする代わりに、リスト内の値の「一番下に到達」 traversed



役立ちます。 ただし、今回は、 Lens'



代わりにTraversal'



タイプを使用します。



Traversal'



は同じLens'



、より弱いだけです:

 type Traversal' ab = forall f . (Applicative f) => (b -> fb) -> (a -> fa)
      
      





Traversal'



and Lens'



コンポジションを作成すると、より弱いタイプ、つまりTraversal'



ます。 これは、結合する順序に関係なく機能します。

 (.) :: Lens' ab -> Traversal' bc -> Traversal' ac (.) :: Traversal' ab -> Lens' bc -> Traversal' ac units :: Lens' Game [Unit] units.traversed :: Traversal' Game Unit units.traversed.health :: Traversal' Game Int
      
      





実際、これを知る必要さえありません。 コンパイラーは、型自体を正しく検出します。

 >>> :t units.traversed.health units.traversed.health :: Applicative f => (Int -> f Int) -> Game -> f Game
      
      





これはTraversal' Game Int



!の定義とTraversal' Game Int



一致します。



実際、これら2つのレンズを1つに組み合わせてみませんか?

 partyHP :: Traversal' Game Int partyHP = units.traversed.health fireBreath :: StateT Game IO () fireBreath = do lift $ putStrLn "*rawr*" partyHP -= 3
      
      





また、 partyHP



関数を使用して、健康の新しい意味を見つけましょう。

 >>> newState <- execStateT fireBreath initialState *rawr* >>> newState^.partyHP <interactive>:3:11: No instance for (Data.Monoid.Monoid Int) arising from a use of `partyHP' .........
      
      





おっと! 私たちは健康の唯一の価値を得ることができないため、これは型の間違いです! これが、 Traversal'



Lens'



より弱いTraversal'



理由Traversal'



。バイパスは多くの値を指すことができるため、単一の値を表示する明確な方法をサポートしていません。 このシステムは、可能性のあるバグを取り除くのに役立ちました。



代わりに、 toListOf



関数を使用してリストを取得することを決定する必要がtoListOf



ます。

 toListOf :: Traversal' ab -> a -> [b]
      
      





これにより、満足のいく結果が得られます。

 >>> toListOf partyHP newState [7,12,5]
      
      





またはtoListOf



関数に相当する中置toListOf



(^..)





 >>> initialState^..partyHP [10,15,8] >>> newState^..partyHP [7,12,5]
      
      





これにより、 fireBreath



fireBreath



なものが得られたことを明確に把握できます。



そして、本当に素晴らしいものを手に入れましょう。 リスティングは地域ごとに決定できます。 できますか?

 around :: Point -> Double -> Traversal' Unit Unit around center radius = filtered (\unit -> (unit^.position.x - center^.x)^2 + (unit^.position.y - center^.y)^2 < radius^2 )
      
      





もちろんできます! 火の息を円に制限することができました!

filtered



は要素の数を格納しないため、実際には理論的には列挙できません


 fireBreath :: Point -> StateT Game IO () fireBreath target = do lift $ putStrLn "*rawr*" units.traversed.(around target 1.0).health -= 3
      
      





コードの表現力に注目してください-ターゲット周辺の全員の健康を低下させます。 このコードは、主要な命令型言語の同等のものよりもはるかに多くを示しています。 そして、結果のコードはエラーの余地をはるかに少なくします。



いずれにせよ、消火器に戻りましょう。 まず、彼の隣にいる人を見てみましょう:

 > initialState^..units.traversed.position [Point {_x = 3.5, _y = 7.0},Point {_x = 1.0, _y = 1.0},Point {_x = 0.0, _y = 2.1}]
      
      





うーん、2人の戦士はお互いに近いです。 そこに火の玉を投げましょう。

 >>> newState <- execStateT (fireBreath (Point 0.5 1.5)) initialState *rawr* >>> (initialState^..partyHP, newState^..partyHP) ([10,15,8],[10,12,5])
      
      





わかった!



スケーリング





レンズを使用すると、よりユニークなことができます。 たとえば、グローバル状態のサブセットをスケーリングします。

 retreat :: StateT Game IO () retreat = do lift $ putStrLn "Retreat!" zoom (units.traversed.position) $ do x += 10 y += 10
      
      





前と同じように、まだ使用する場合は、2つのレンズを1つにまとめることができます。

 partyLoc :: Traversal' Game Point partyLoc = units.traversed.position retreat :: StateT Game IO () retreat = do lift $ putStrLn "Retreat!" zoom partyLoc $ do x += 10 y += 10
      
      





さあ、やってみましょう!

 >>> initialState^..partyLoc [Point {_x = 3.5, _y = 7.0},Point {_x = 1.0, _y = 1.0},Point {_x = 0.0, _y = 2.1}] >>> newState <- execStateT retreat initialState Retreat! >>> newState^..partyLoc [Point {_x = 13.5, _y = 17.0},Point {_x = 11.0, _y = 11.0},Point {_x = 10.0, _y = 12.1}]
      
      





コンテキストでスケーリングのタイプを詳しく見てみましょう。

 zoom :: Traversal ab -> StateT b IO r -> StateT a IO r
      
      





zoom



機能には、いくつかの大きな理論的可能性があります。 たとえば、スケーリングされた2倍レンズの構成は、その構成のスケーリングと同じ結果になるはずです。

 zoom lens1 . zoom lens2 = zoom (lens1 . lens2)
      
      





そして、空のレンズをスケーリングすると、それ自体が得られます:

 zoom id = id
      
      





つまり、 zoom



関数はファンクターです。つまり、ファンクターの法則に従います。



チームを結合する


その前に、一度に1つのチームを見ましたが、今度はコンセプトを組み合わせて、キャラクター間の戦いを強制的に設定しましょう。

 battle :: StateT Game IO () battle = do -- ! forM_ ["Take that!", "and that!", "and that!"] $ \taunt -> do lift $ putStrLn taunt strike --  ! fireBreath (Point 0.5 1.5) replicateM_ 3 $ do --  ! retreat --    zoom (boss.position) $ do x += 10 y += 10
      
      





さあ、行こう!

 >>> execStateT battle initialState Take that! *shink* and that! *shink* and that! *shink* *rawr* Retreat! Retreat! Retreat! Game {_score = 0, _units = [Unit {_health = 10, _position = Poin t {_x = 33.5, _y = 37.0}},Unit {_health = 12, _position = Point {_x = 31.0, _y = 31.0}},Unit {_health = 5, _position = Point {_x = 30.0, _y = 32.1}}], _boss = Unit {_health = 70, _position = P oint {_x = 30.0, _y = 30.0}}}
      
      





Haskellが最高の命令型言語であると言うとき、人々は本当に冗談を言っているのではないと思います!



おわりに



Haskellエコシステムの王室の宝物の1つと考えられているlens



ライブラリのカーテンを開きました。 強力で複雑な構成を非常に読みやすくエレガントなコードに圧縮するために、純粋なプログラミングにレンズを使用することもできます。 ただし、この素晴らしいライブラリについてはまだ多くを書くことができます。



All Articles