タイプファミリーとポケモン

まえがき



Haskellの勉強を始めたとき、私はほとんどすぐに打たれました。 まず、現在の作業プロジェクトに真っ向から取り組み、これらのライブラリのほとんどがGHC(Glasgow Haskell Compiler)にのみ存在する言語拡張機能を使用していることを発見しました。 これは、主に、1つのサプライヤにしか存在しない拡張機能を使用する必要があるほど弱い言語を使用したいため、少し気になりました。 そうですか?

さて、私はそれをもう一度マスターし、これらの拡張機能に関するすべてを調べることにしました。一般的な代数データ型、ファミリー型、および機能依存性という、同様の問題を解決したHaskellコミュニティの3つのホットトピックを取り上げました。 それらについて教えるリソースを見つけようとしても、それが何であり、どのように使用するかを説明する記事しか見つけることができませんでした。 しかし、実際、誰もそれらが必要な理由を説明しませんでした!..したがって、私はフレンドリーな例を使用してこの記事を書くことにしました。







ポケモンのことを聞いたことがありますか? これらはポケモンの世界に生息する素晴らしい生き物です。 あなたは彼らが並外れた能力を持つ動物のようであると仮定することができます。 すべてのポケモンは要素を所有しており、すべての機能はこの要素に依存しています。 たとえば、火の要素のポケモンは火を吐くことができますが、水の要素のポケモンは水ジェットでスプレーできます。

ポケモンは人々のものであり、彼らの特別な能力は生産的な活動に役立てるために使用できますが、一部の人々は他の人のポケモンと戦うために自分のポケモンを使用します。 これらの人々は自分自身をポケモントレーナーと呼びます。 最初は動物虐待のように聞こえるかもしれませんが、それはとても楽しく、ポケモンを含む誰もが幸せそうです。 ポケモンの世界では、10歳の子供が最高のポケモントレーナーになるために命を危険にさらすために家を出たとしても、誰も誰もいないように、すべてが正常に見えるようになっていることに留意してください。



Haskellを使用して、ポケモンの世界の限られた(そして多少簡素化された、ファンに許してもらう)部分を提示します。 すなわち:



最初の試み



まず、型クラスと型ファミリを使用せずにルールを実装してみましょう。

ポケモンのいくつかの要素とその動きから始めましょう。 ポケモンの種類とその動きを区別するのに役立つため、個別に実装します。

このために、ポケモンの動きを選択して、各ポケモンの機能を定義します。



data Fire = Charmander | Charmeleon | Charizard deriving Show data Water = Squirtle | Wartortle | Blastoise deriving Show data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show data FireMove = Ember | FlameThrower | FireBlast deriving Show data WaterMove = Bubble | WaterGun deriving Show data GrassMove = VineWhip deriving Show pickFireMove :: Fire -> FireMove pickFireMove Charmander = Ember pickFireMove Charmeleon = FlameThrower pickFireMove Charizard = FireBlast pickWaterMove :: Water -> WaterMove pickWaterMove Squirtle = Bubble pickWaterMove _ = WaterGun pickGrassMove :: Grass -> GrassMove pickGrassMove _ = VineWhip
      
      





これまでのところ、タイプチェッカーは、どのポケモンがその要素を正しく使用しているかを把握するのに役立ちます。





私たちが説明した9つのポケモンのうち6つは、3つの要素すべてから成り立っています。 タイプごとに2



今、私たちは戦いを実現しなければなりません。 バトルは、各ポケモンがどのようにヒットしたかを説明するメッセージの出力であり、次のように勝者を示します。



 printBattle :: String -> String -> String -> String -> String -> IO () printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do putStrLn $ pokemonOne ++ " used " ++ moveOne putStrLn $ pokemonTwo ++ " used " ++ moveTwo putStrLn $ "Winner is: " ++ winner ++ "\n"
      
      





これは単なる動きの反映であり、ポケモンとその打撃の要素に基づいて勝者を見つけなければなりません。 以下は、火と水の要素の間の戦いの機能の例です。



 battleWaterVsFire :: Water -> Fire -> IO () battleWaterVsFire water fire = do printBattle (show water) moveOne (show fire) moveTwo (show water) where moveOne = show $ pickWaterMove water moveTwo = show $ pickFireMove fire battleFireVsWater = flip battleWaterVsFire --   ,   ,   ,   
      
      





これらすべてを組み合わせて、戦闘の他の機能を追加すると、プログラムが作成されます。



型クラスの紹介



繰り返されるコードはいくらですか:誰かがポケモンエレクトリックエレメント、たとえばピカチュウを追加したいと思ったら、独自のbattleElectricVs(Grass|Fire|Water)



戦いを追加する必要があります。 ポケモンとは何か、新しいものを追加する方法をより深く理解するのに役立ついくつかのテンプレートがあります。

私たちは何を持っています:



形式化のためにいくつかの型クラスを定義します。そして、ルールが決まったら、今では珍しい名前スキームの名前も変更します。各スキームには、それが動作する要素が含まれます。



ポケモンクラス


ポケモンクラスは、ポケモンがその動きを選択したという知識を表示します。 これにより、同じ関数がクラスが定義されているさまざまな要素で動作できるように、 pickMove



を定義pickMove



できるようになります。



「バニラ」クラスとは異なり、ポケモンのクラスには2種類が必要です。ポケモンの要素とそれらが使用するダメージの種類であり、後で一方が他方に依存します。 クラスに2つのパラメーターを許可する言語拡張を含める必要があります: MultiParamTypeClasses





ポケモンとそのヒットを表示できるように制限を追加する必要があることに注意してください。

定義と、既存のポケモン要素のいくつかのインスタンスを次に示します。



 class (Show pokemon, Show move) => Pokemon pokemon move where pickMove :: pokemon -> move instance Pokemon Fire FireMove where pickMove Charmander = Ember pickMove Charmeleon = FlameThrower pickMove Charizard = FireBlast instance Pokemon Water WaterMove where pickMove Squirtle = Bubble pickMove _ = WaterGun instance Pokemon Grass GrassMove where pickMove _ = VineWhip
      
      





そして、このような関数を使用できます



 pickMove Charmander :: FireMove
      
      





物事がどのように乱雑に見え始めるかに注意してください。 ポケモンの要素と動きのタイプは、タイプクラスごとに独立して処理されるためです。 Fire Strikeを選択すると、タイプインスペクターにすべての情報を提供し、使用するクラスとストロークを決定します。

バトルクラス


すでに自分のパンチを選択できるポケモンがありますが、 battle*family*Vs*family



などの機能を取り除くために、2つのポケモンのバトルを表す抽象化が必要になりました

私たちは本当にこのようなコードを書きたいと思います:



 class (Pokemon pokemon move, Pokemon foe foeMove) => Battle pokemon move foe foeMove where battle :: pokemon -> foe -> IO () battle pokemon foe = do printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon) where move = pickMove pokemon foeMove = pickMove foe instance Battle Water WaterMove Fire FireMove
      
      





ただし、実行すると、すべての種類のストロークを考慮した一般的なインスタンスがなくなるため、型チェッカーからエラーが発生します。

この問題は解決されましたが、最終的なコードは見苦しくなります。実際には、バトル関数のタイプを

 battle :: pokemon -> foe -> IO (move, foeMove)
      
      





最後に、タイプファミリの紹介!



まあ、私たちのプログラムは気のめいるようです。 すべてのタイプシグネチャを処理する必要があります。コンパイラを支援するためにタイプシグネチャを使用できるように、関数の内部動作( battle



)のみを変更する必要さえあります。 さらに進んで、現在のプログラムのリファクタリングは、コードに非常に多くの不名誉をもたらした後、それほど正式ではなく、あまり再現性がなく、それほど成果ではないと言うことができます。

これで、ポケモンクラスの定義を振り返ることができます。 ポケモンの要素とストロークのタイプが2つの別個のクラス変数としてあります。 タイプチェッカーは、ポケモンの要素とストロークのタイプの関係を認識しません。 それは、水ポケモンが火のストライキを作成するとき、ポケモンのインスタンスを決定することさえ可能にします!

これがタイプファミリーの出番です。タイプインスペクターに、FirePokémonはFireストライキなどでのみ動作できると言わせます。

タイプファミリーを使用したポケモンクラス


タイプファミリを使用するには、 TypeFamilies



拡張機能を有効にする必要があります。 プラグインするとすぐに、次のスタイルでクラスを記述できます。



 {-# LANGUAGE TypeFamilies, FlexibleContexts #-} class (Show p, Show (Move p)) => Pokemon p where data Move p :: * pickMove :: p -> Move p
      
      





ポケモンクラスは、1つの引数と1つの関連付けられたタイプのMovementを持つように定義しました。 Movementのタイプは、使用するストロークのタイプを返す「タイプ関数」になります。 これは、 WaterMove



代わりにMove Fire



WaterMove



代わりにWaterMove



Move Fire



を使用することを意味します。

依存関係は前の例とほとんど同じように見えることに注意してください。Showmoveの代わりにShow (Move a))



を使用します。 もう1つ追加する必要があります。それを使用するには、 FlexibleContexts



です。

現在、Haskellは優れた構文糖衣を提供しているため、インスタンスを定義するときに、実際の関連データコンストラクターを右側で定義できます。

すべてのデータ型を再定義し、型ファミリを使用して必要なクラスインスタンスを作成しましょう。



 data Fire = Charmander | Charmeleon | Charizard deriving Show instance Pokemon Fire where data Move Fire = Ember | FlameThrower | FireBlast deriving Show pickMove Charmander = Ember pickMove Charmeleon = FlameThrower pickMove Charizard = FireBlast data Water = Squirtle | Wartortle | Blastoise deriving Show instance Pokemon Water where data Move Water = Bubble | WaterGun deriving Show pickMove Squirtle = Bubble pickMove _ = WaterGun data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show instance Pokemon Grass where data Move Grass = VineWhip deriving Show pickMove _ = VineWhip
      
      





これで安全に書くことができます

 pickMove Squirtle
      
      





結果を取得します。

それは美しいですね。 パンチを選択するために署名を書く必要はもうありません。

ただし、初期バージョンと比較するには時期尚早です。 最終結果を比較して、表示内容の完全な効果を取得することをお勧めします。

新しいバトルクラス


長い署名が不要になったため、不快な松葉杖を削除して、ほぼ元の値に戻すことができます。



 class (Pokemon pokemon, Pokemon foe) => Battle pokemon foe where battle :: pokemon -> foe -> IO () battle pokemon foe = do printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon) where foeMove = pickMove foe move = pickMove pokemon
      
      





そして、気をつけて、今ではバトルはストライキについて何も知る必要がありません。 そして、ポケモンを打ち負かすことは、素朴な実装とほとんど同じように見えます。



 instance Battle Water Fire instance Battle Fire Water where battle = flip battle instance Battle Grass Water instance Battle Water Grass where battle = flip battle instance Battle Fire Grass instance Battle Grass Fire where battle = flip battle
      
      





使用も簡単です。

  battle Squirtle Charmander
      
      





以上です! 私たちのプログラムはようやく見栄えが良くなり、改善され、型チェッカーがより多くをチェックし、繰り返しを少なくし、他の開発者に提供するためのきれいなAPIを持っています。



いいね! やった! 楽しんでいただけましたでしょうか!

わかりました ブラウザのスクロールバーに、このフレーズの下にまだ場所があることが示されているため、あなたは楽しんでいて、すべてがすでに終わっているとは信じられないことに気付きました。



さて、ポケモンの世界にもう1つ追加しましょう。

これで、 Water



要素とFire



要素をBattle Water Fire



として識別しました。その後、 Battle Water Fire



は前のものと同じですが、引数が交換されています。 最初のポケモンが常に勝ち、常に以下が出力されます:

 -- Winner Pokemon move -- Loser Pokemon move -- Winner pokemon Wins.
      
      





インスタンスに最初に敗者がいる場合でも、勝者が最初に画面に表示されます。

これを置き換えて、誰が戦いに勝つかを標本に決めさせましょう。

 -- Loser Pokemon move -- Winner Pokemon move -- Winner pokemon Wins
      
      





関連するタイプの同義語



2つのタイプの選択を返すことを決定する場合、通常はEither ab



使用しますが、これは実行時ですが、FireとWaterの要素が戦うときは常にWaterが勝者になることをタイプチェッカーで確認する必要があります。

したがって、バトルに新しい関数を追加し、 勝者と名付けます。これは、バトル関数が受け取ったのと同じ順序で2つの引数を受け取り、誰が勝つかを決定します。

ただし、いくつかのオプションのいずれかを返すと、勝者の署名の選択が不確実になります。



 class Battle pokemon foe where .. winner :: pokemon -> foe -> ??? --   , 'pokemon'  'foe'? instance Battle Water Fire where winner :: Water -> Fire -> Water -- Water    : pokemon winner water _ = water instance Battle Fire Water where winner :: Fire -> Water -> Water -- Water   : foe winner _ water = water
      
      





Battle Water Fire



インスタンスの場合、勝者のタイプはpokemon



と同じであり、 Battle Fire Water



場合は既にfoe



なっています。



幸いなことに、タイプファミリは関連するタイプシノニムもサポートしています。 バトルクラスでは、 Winner pokemon foo



があります。インスタンスでは、どれを使用するかを決定します。 pokemon



またはfoe



同義語であるため、データではなく型を使用します。

Winner



は型署名された型関数* -> * -> *



であり、 pokemon



foo



両方を受け取り、そのうちの1つを返します。

pokemon



を選択するデフォルトの実装も定義します



 class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where type Winner pokemon foe :: * --    type Winner pokemon foe = pokemon --      battle :: pokemon -> foe -> IO () battle pokemon foe = do printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner) where foeMove = pickMove foe move = pickMove pokemon winner = pickWinner pokemon foe pickWinner :: pokemon -> foe -> (Winner pokemon foe)
      
      





インスタンスは次のように作成されます。



 instance Battle Water Fire where pickWinner pokemon foe = pokemon instance Battle Fire Water where type Winner Fire Water = Water pickWinner = flip pickWinner
      
      





これですべてです。

プログラムの最終バージョンは次のとおりです。

ポケモンの戦いの最終版
 {-# LANGUAGE TypeFamilies, MultiParamTypeClasses, FlexibleContexts #-} class (Show pokemon, Show (Move pokemon)) => Pokemon pokemon where data Move pokemon :: * pickMove :: pokemon -> Move pokemon data Fire = Charmander | Charmeleon | Charizard deriving Show instance Pokemon Fire where data Move Fire = Ember | FlameThrower | FireBlast deriving Show pickMove Charmander = Ember pickMove Charmeleon = FlameThrower pickMove Charizard = FireBlast data Water = Squirtle | Wartortle | Blastoise deriving Show instance Pokemon Water where data Move Water = Bubble | WaterGun deriving Show pickMove Squirtle = Bubble pickMove _ = WaterGun data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show instance Pokemon Grass where data Move Grass = VineWhip deriving Show pickMove _ = VineWhip printBattle :: String -> String -> String -> String -> String -> IO () printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do putStrLn $ pokemonOne ++ " used " ++ moveOne putStrLn $ pokemonTwo ++ " used " ++ moveTwo putStrLn $ "Winner is: " ++ winner ++ "\n" class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where type Winner pokemon foe :: * type Winner pokemon foe = pokemon battle :: pokemon -> foe -> IO () battle pokemon foe = do printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner) where foeMove = pickMove foe move = pickMove pokemon winner = pickWinner pokemon foe pickWinner :: pokemon -> foe -> (Winner pokemon foe) instance Battle Water Fire where pickWinner pokemon foe = pokemon instance Battle Fire Water where type Winner Fire Water = Water pickWinner = flip pickWinner instance Battle Grass Water where pickWinner pokemon foe = pokemon instance Battle Water Grass where type Winner Water Grass = Grass pickWinner = flip pickWinner instance Battle Fire Grass where pickWinner pokemon foe = pokemon instance Battle Grass Fire where type Winner Grass Fire = Fire pickWinner = flip pickWinner main :: IO () main = do battle Squirtle Charmander battle Charmeleon Wartortle battle Bulbasaur Blastoise battle Wartortle Ivysaur battle Charmeleon Ivysaur battle Venusaur Charizard
      
      







これで、独自の電動ポケモンを追加できます! 試してみてください!



PSオリジナル記事タイプファミリーとポケモン

この記事は、インタラクティブな対話を目的としているため、要約されています。



All Articles