レンズ
エレガントなコードへのチケットは、レンズライブラリです。
通常どおりデータを定義し、フィールド名の先頭にアンダースコアのみを追加します。 たとえば、ゲームを次のように定義できます。
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は間違いなくマルチパラダイム言語ではありませんが、マルチパラダイムコードに現れるものがあります。
信じられないほど、このコードには言語に組み込まれた機能はありません!
-
boss
とhealth
は上で定義したレンズにすぎません -
(-=)
-中置関数 -
(.)
-HaskelPrelude
機能的構成!
待って、
(.)
機能的な構成ですか?! ほんと?
これは、すべてのレンズマジックが発生する場所です。 レンズは最も一般的な機能であり、「マルチパラダイム」コードはすべて、機能の混合物にすぎません!
実際、
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
この知識を武器に、
と
機能のタイプをどのように拡張できるか見てみましょう。
次に、機能構成の定義を見てみましょう。
型変数を次のように置き換えた場合に注意してください。
次に、2つのレンズの構成に対して完全に一意の対応を取得します。
と同義語を逆にすると、次のようになります。
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
ライブラリのカーテンを開きました。 強力で複雑な構成を非常に読みやすくエレガントなコードに圧縮するために、純粋なプログラミングにレンズを使用することもできます。 ただし、この素晴らしいライブラリについてはまだ多くを書くことができます。