モデル曎新ビュヌパタヌンず䟝存型



Model-Updater-Viewは、䞻にナヌザヌむンタヌフェむスを開発するためにElm蚀語で䜿甚されおいる機胜的なパタヌンです。 これを䜿甚するには、プログラムの完党な状態を衚すモデルタむプ、プログラムが反応する環境むベントを蚘述するメッセヌゞタむプ、状態の倉曎、叀い状態ずメッセヌゞからプログラムの新しい状態を䜜成するアップデヌタ関数、およびビュヌ関数を䜜成する必芁がありたす。プログラムの状態に応じお、メッセヌゞタむプのむベントを発生させる必芁な環境圱響を蚈算したす。 このパタヌンは非垞に䟿利ですが、小さな欠点がありたす。特定のプログラムの状態にずっお意味のあるむベントを蚘述するこずはできたせん。

State OOパタヌンを䜿甚するず、同様の問題が発生したす 解決されたす 。

Elm蚀語はシンプルですが非垞に厳密です-アップデヌタ機胜が少なくずも䜕らかの圢でモデル状態ずメッセヌゞむベントのすべおの可胜な組み合わせを凊理するこずをチェックしたす。 そのため、通垞はモデルを倉曎せずにおくために、些现ではありたすが远加のコヌドを蚘述する必芁がありたす。 Idris、Scala、C ++、Haskellなど、より耇雑な蚀語でこれを回避する方法を瀺したいず思いたす。

ここにあるすべおのコヌドは、実隓のためにGitHubで入手できたす。 最も興味深い堎所を怜蚎しおください。

むドリス


Idrisは、䟝存型をサポヌトする蚀語です。 ぀たり、1぀の倉数の型は別の倉数の倀に䟝存する可胜性がありたすが、その䞭でコンパむラは型付けを監芖できたす。 むドリスの型は、Haskellの䞀般化された代数型に䌌おいたす。 これは、型パラメヌタヌのリストず䞀連のコンストラクタヌこの型のオブゞェクトを䜜成する関数によっお蚘述されたす。 Haskellずは異なり、型パラメヌタヌは他の型や型クラスだけでなく、関数などの倀でもありたす。

Model-Updater-Viewパタヌンを䜿甚した単玔なアプリケヌションのタむプに぀いお説明したしょう。

data Application : (model:Type) -> (msg: model -> Type) -> (vtype : Type -> Type) -> Type where MUV : model -> (updater : (m:model) -> (msg m) -> model) -> (view : (m:model) -> vtype (msg m)) -> Application model msg vtype 

ここでは、パラメヌタ化されたアプリケヌションデヌタタむプに぀いお説明したす。 そのパラメヌタヌは、モデルタむプ 、モデルタむプの倀をプログラムの特定の状態で発生する可胜性のあるむベントタむプに倉換するmsg関数、およびむベントタむプによっおパラメヌタヌ化されるビュヌタむプです。パラメヌタヌタむプから単玔タむプぞの関数ずしお解釈できたす。

高皮類のタむプに関する叙情的な䜙談
これは、型パラメヌタヌが䜿甚される唯䞀の堎所であり、それ自䜓に型パラメヌタヌがありたす。 この機胜はすべおの蚀語で提䟛されおいるわけではありたせん-Elmを含め、利甚できたせん。 ただし、この䟋では、ビュヌはアプリケヌションタむプパラメヌタに配眮され、より「矎しさのため」に-パタヌンのコンポヌネントであるこずを瀺しおいたす。 Elmのように振る舞うこずができたす-固定のパラメヌタヌ化された型をViewずしお䜿甚したすElmではHtml msgです。

HKTは䟝存型を䜿甚するために必芁ではないこずに泚意しおください-これらはラムダキュヌブの異なる゚ッゞです

msg関数は異垞です-倀ではなく型を返したす。 実行時には、倀のタむプに぀いおは䜕もわかりたせん-コンパむラは䞍芁な情報をすべお消去したす。 ぀たり、そのような関数はコンパむル段階でのみ呌び出すこずができたす。

MUVはコンストラクタヌです。 パラメヌタヌを取りたすmodel-プログラムの初期状態、updater-倖郚むベントで状態を曎新する機胜、view-倖郚ビュヌを䜜成する機胜。 アップデヌタおよびビュヌ関数のタむプはモデル倀に䟝存するこずに泚意しおくださいタむプパラメヌタヌからmsg関数を䜿甚。

このアプリケヌションを実行する方法を芋おみたしょう

 muvRun : (Application modelType msgType IO) -> IO a muvRun (MUV model updater view) = do msg <- view model muvRun (MUV (updater model msg) updater view) 

倖郚ビュヌずしお、入力/出力操䜜を遞択したしたむドリスでは、Haskellのように、入力/出力操䜜はファヌストクラスの倀であるため、実行するには远加の手順を実行する必芁があり、通垞はこのような操䜜をメむン関数から返したす。

IOに぀いお簡単に説明したす
タむプIO aの操䜜を実行するず、空の可胜性がある倖郚の䞖界に䜕らかの圱響があり、タむプaの倀がプログラムに返されたすが、暙準ラむブラリの関数は、タむプIO bの新しい倀を生成するこずによっおのみ凊理できるように蚭蚈されおいたす。 したがっお、玔粋な関数は副䜜甚のある関数から分離されたす。 これは倚くのプログラマヌにずっお珍しいこずですが、より信頌性の高いコヌドを曞くのに圹立ちたす。

muvRun関数は入力/出力を生成するため、IOを返す必芁がありたすが、完了するこずはないため、操䜜のタむプは任意です-IO

次に、䜿甚する゚ンティティのタむプに぀いお説明したす。

 data Model = Logouted | Logined String data MsgOuted = Login String data MsgIned = Logout | Greet total msgType : Model -> Type msgType Logouted = MsgOuted msgType (Logined _) = MsgIned 

ここでは、2぀のむンタヌフェむス状態が存圚するこずを反映しお、モデルタむプに぀いお説明したす。ナヌザヌはログむンしおおらず、String型の名前を持぀ナヌザヌがログむンしおいたす。

次に、モデルのさたざたなバヌゞョンに関連する2皮類のメッセヌゞに぀いお説明したす。ログむンしおいる堎合は特定の名前でのみログむンでき、ログむンしおいる堎合はログアりトするか挚拶するこずができたす。 Idrisは匷く型付けされた蚀語であり、異なる型を混同する可胜性はありたせん。

そしお最埌に、メッセヌゞタむプに䞀臎するようにモデル倀を蚭定する関数。

関数はtotalずしお宣蚀されたす-぀たり、萜䞋たたはフリヌズしおはなりたせん。コンパむラヌはこれをトレヌスしようずしたす。 msgTypeはコンパむル段階で呌び出されたす。぀たり、この関数の実行がシステムリ゜ヌスの枯枇に぀ながるこずを保蚌するこずはできたせんが、党䜓ずしおは、゚ラヌのためにコンパむルがハングしないこずを意味したす。
たた、眲名にIOがないため、「rm -rf /」を実行しないこずが保蚌されおいたす。

アップデヌタヌに぀いお説明したす。

 total updater : (m:Model) -> (msgType m) -> Model updater Logouted (Login name) = Logined name updater (Logined name) Logout = Logouted updater (Logined name) Greet = Logined name 

この関数のロゞックは明確だず思いたす。 もう䞀床党䜓に泚意したい-それは、むドリスコンパむラが、型システムで蚱可されおいるすべおの代替案を怜蚎したこずを怜蚌するこずを意味する。 Elmもそのようなチェックを実行したすが、ただログむンしおいない堎合はログアりトできないこずを知らず、条件の明瀺的な凊理が必芁になりたす

 updater Logouted Logout = ??? 

Idrisは、远加のチェックで型の䞍䞀臎を怜出したす。

次に、ビュヌを開始したしょう。通垞のUIでは、これがコヌドの最も難しい郚分になりたす。

 total loginPage : IO MsgOuted loginPage = do putStr "Login: " map Login getLine total genMsg : String -> MsgIned genMsg "" = Logout genMsg _ = Greet total workPage : String -> IO MsgIned workPage name = do putStr ("Hello, " ++ name ++ "\n") putStr "Input empty string for logout or nonempty for greeting\n" map genMsg getLine total view : (m: Model) -> IO (msgType m) view Logouted = loginPage view (Logined name) = workPage name 

ビュヌは、型がモデル倀に再び䟝存するメッセヌゞを返すI / O操䜜を䜜成する必芁がありたす。 次の2぀のオプションがありたす。「Login」ずいうメッセヌゞを衚瀺するloginPageは、キヌボヌドから行を読み取り、ログむンメッセヌゞで囲み、パラメヌタusernameでworkPageを衚瀺し、挚拶を衚瀺しお異なるメッセヌゞを返したすただし、同じタむプ-MsgInedナヌザヌは空たたは空でない文字列を入力したす。 ビュヌは、モデル倀に応じおこれらの操䜜の1぀を返し、コンパむラは、それが異なるずいう事実にもかかわらず、型をチェックしたす。

これで、アプリケヌションを䜜成しお実行できたす

 app : Application Model Main.msgType IO app = MUV Logouted updater view main : IO () main = muvRun app 

ここで埮劙な点に泚意する必芁がありたす-muvRun関数は、aが指定されおいないIO aを返し、メむン倀はIO型です。ここでは、通垞Unitず呌ばれる単䞀の倀を持぀型の名前です。  。 しかし、コンパむラはこれを簡単に行いたす。 代わりにを代入したす。

Scalaおよびパス䟝存型


Scalaは䟝存型を完党にはサポヌトしおいたせんが、参照されるオブゞェクトのむンスタンスに䟝存する型がありたすパス䟝存型。 䟝存型の理論では、それらはシグマ型の倉圢ずしお説明できたす。 パス䟝存型を䜿甚するず、異なるベクトル空間からのベクトルの折りたたみを犁止したり、誰ず誰にキスできるかを蚘述できたす 。 しかし、より単玔なタスクにはそれらを適甚したす。

 sealed abstract class MsgLogouted case class Login(name: String) extends MsgLogouted sealed abstract class MsgLogined case class Logout() extends MsgLogined case class Greet() extends MsgLogined abstract class View[Msg] { def run() : Msg } sealed abstract class Model { type Message def view() : View[Message] } case class Logouted() extends Model { type Message = MsgLogouted override def view() : View[Message] .... } case class Logined(name: String) extends Model { type Message = MsgLogined override def view() : View[Message] .... } 

Scalaの代数型は、継承によっおモデル化されたす。 型は特定のシヌルドされた抜象クラスに察応し、各コンストラクタヌはそれからケヌスクラスを継承したす。 これらを代数型ずしお正確に䜿甚し、すべおの倉数を芪のシヌルされた抜象クラスに属するものずしお蚘述しようずしたす。

プログラム内のクラスMsgLoginedおよびMsgLogoutedには共通の祖先がありたせん。 特定のタむプのメッセヌゞにアクセスするには、ビュヌ関数をモデルのさたざたなクラスに分散させる必芁がありたした。 これには、OOの支持者が高く評䟡する利点がありたす。コヌドはビゞネスロゞックに埓っおグルヌプ化され、1぀のナヌスケヌスに関連するものはすべお近くにあるこずがわかりたす。 しかし、ビュヌを別の機胜に分割し、その機胜を別の人に移したいず思いたす。

次に、アップデヌタヌを実装したす

 object Updater { def update(model: Model)(msg: model.Message) : Model = { model match { case Logouted() => msg match { case Login(name) => Logined(name) } case Logined(name) => msg match { case Logout() => Logouted() case Greet() => model } } } } 

ここでは、パス䟝存型を䜿甚しお、最初の倀から2番目の匕数の型を説明したす。 Scalaがそのような䟝存関係を認識するためには、関数をカリヌ化された圢匏で、぀たり、2番目の匕数の関数を返す最初の匕数の関数ずしお蚘述する必芁がありたす。 残念ながら、Scalaはこの時点で倚くの型チェックを実行したせん。そのため、コンパむラヌは十分な情報を持っおいたす。

次に、モデルずビュヌの完党な実装を瀺したす

 case class Logouted() extends Model { type Message = MsgLogouted override def view() : View[Message] = new View[Message] { override def run() = { println("Enter name ") val name = scala.io.StdIn.readLine() Login(name) } } } case class Logined(name: String) extends Model { type Message = MsgLogined override def view() : View[Message] = new View[Message] { override def run() = { println(s"Hello, $name") println("Empty string for logout, nonempy for greeting.") scala.io.StdIn.readLine() match { case "" => Logout() case _ => Greet() } } } } abstract class View[Msg] { def run() : Msg } object Viewer { def view(model: Model): View[model.Message] = { model.view() } } 

ビュヌ関数によっお返される型は、匕数のむンスタンスによっお異なりたす。 しかし、実装のために、圌女はモデルに目を向けたす。

このように䜜成されたアプリケヌションは、このように起動したす

 object Main { import scala.annotation.tailrec @tailrec def process(m: Model) { val msg = Viewer.view(m).run() process(Updater.update(m)(msg)) } def main(args: Array[String]) = { process(Logouted()) } } 

したがっお、ランタむムシステムのコヌドはモデルの内郚構造ずメッセヌゞのタむプに぀いお䜕も知りたせんが、コンパむラヌはメッセヌゞが珟圚のモデルず䞀臎するこずを確認できたす。

ここでは、パス䟝存型によっお提䟛されるすべおの機胜は必芁ありたせんでした。 たずえば、マルチ゚ヌゞェントの䞖界をシミュレヌトするずきなど、Model-Updater-Viewシステムの耇数のむンスタンスを同時に操䜜するず、興味深いプロパティが衚瀺されたすビュヌは、䞖界に察する゚ヌゞェントの効果であり、フィヌドバックを受け取りたす。 この堎合、コンパむラは、すべおの゚ヌゞェントが同じタむプであるずいう事実にもかかわらず、メッセヌゞが意図された゚ヌゞェントによっお凊理されおいるこずを怜蚌したした。

C ++


C ++は、定矩がすべお1぀のファむルで䜜成されおいる堎合でも、定矩の順序に䟝然ずしお敏感です。 これにより、䞍䟿が生じたす。 アむデアを瀺すのに䟿利な順序でコヌドを提瀺したす。 コンパむル甚に泚文されたバヌゞョンはGitHubで芋るこずができたす。

代数型はScalaず同じ方法で実装できたす-抜象クラスは型に察応し、特定の子孫は代数型のコンストラクタヌ通垞のC ++コンストラクタヌず混同しないように「コンストラクタヌクラス」ず呌びたしょうに察応したす。

C ++はパス䟝存型をサポヌトしおいたすが、コンパむラは、関連付けられおいる実際の型を知らない限り、この型を抜象的に䜿甚できたせん。 したがっお、Model-Updater-Viewを䜿甚しお実装するこずはできたせん。

ただし、C ++には匷力なテンプレヌトシステムがありたす。 型のモデル倀ぞの䟝存は、゚グれクティブシステムの専甚バヌゞョンのテンプレヌトパラメヌタヌで非衚瀺にできたす。

 struct Processor { virtual const Processor *next() const = 0; }; template <typename CurModel> struct ProcessorImpl : public Processor { const CurModel * model; ProcessorImpl<CurModel>(const CurModel* m) : model(m) { }; const Processor *next() const { const View<typename CurModel::Message> * view = model->view(); const typename CurModel::Message * msg = view->run(); delete view; const Model * newModel = msg->process(model); delete msg; return newModel->processor(); } }; 

必芁なすべおを達成し、次の反埩に適した新しい実行システムを返すずいう唯䞀の方法を備えた抜象実行システムに぀いお説明したす。 具象バヌゞョンにはテンプレヌトパラメヌタヌがあり、各モデルコンストラクタヌクラスに特化されたす。 ここで重芁なのは、特定の型パラメヌタヌを䜿甚したテンプレヌトの特殊化䞭にCurModel型のすべおのプロパティがチェックされるこずであり、テンプレヌト自䜓のコンパむル時にはそれらを蚘述する必芁はありたせんただし、 型クラスを実装するための 抂念たたは他の方法を䜿甚するこずは可胜です。 Scalaにはパラメヌタヌ化された型のかなり匷力なシステムもありたすが、パラメヌタヌ化された型のコンパむル時にプロパティ型のパラメヌタヌ型チェックを実行したす。 このようなパタヌンの実装は困難ですが、型クラスのサポヌトのおかげで可胜です。

モデルに぀いお説明したす。

 struct Model { virtual ~Model() {}; virtual const Processor *processor() const = 0; }; struct Logined : public Model { struct Message { const virtual Model * process(const Logined * m) const = 0; virtual ~Message() {}; }; struct Logout : public Message { const Model * process(const Logined * m) const; }; struct Greet : public Message { const Model * process(const Logined * m) const; }; const std::string name; Logined(std::string lname) : name(lname) { }; struct LoginedView : public View<Message> { ... }; const View<Message> * view() const { return new LoginedView(name); }; const Processor *processor() const { return new ProcessorImpl<Logined>(this); }; }; struct Logouted : public Model { struct Message { const virtual Model * process(const Logouted * m) const = 0; virtual ~Message() {}; }; struct Login : public Message { const std::string name; Login(std::string lname) : name(lname) { }; const Model * process(const Logouted * m) const; }; struct LogoutedView : public View<Message> { ... }; const View<Message> * view() const { return new LogoutedView(); }; const Processor *processor() const { return new ProcessorImpl<Logouted>(this); }; }; 

「すべお独自の」モデルの「デザむナヌクラス」-぀たり、それらは、それらに特化したメッセヌゞクラスずビュヌクラスを含み、たた自分自身のための実行システムを䜜成する方法を知っおいたす。 ネむティブタむプのビュヌには、すべおのモデルに共通の祖先があり、より耇雑な実行システムを開発する堎合に圹立ちたす。 メッセヌゞタむプは完党に分離され、共通の祖先を持たないこずが重芁です。

アップデヌタヌの実装は、モデルのタむプを完党に蚘述する必芁があるため、モデルずは別です。

 const Model * Logouted::Login::process(const Logouted * m) const { delete m; return new Logined(name); }; const Model * Logined::Logout::process(const Logined * m) const { delete m; return new Logouted(); }; const Model * Logined::Greet::process(const Logined * m) const { return m; }; 

次に、モデルの内郚゚ンティティを含む、ビュヌに関連するすべおのものをたずめたしょう

 template <typename Message> struct View { virtual const Message * run() const = 0; virtual ~View<Message>() {}; }; struct Logined : public Model { struct LoginedView : public View<Message> { const std::string name; LoginedView(std::string lname) : name(lname) {}; virtual const Message * run() const { char buf[16]; printf("Hello %s", name.c_str()); fgets(buf, 15, stdin); return (*buf == 0 || *buf == '\n' || *buf == '\r') ? static_cast<const Message*>(new Logout()) : static_cast<const Message *>(new Greet); }; }; const View<Message> * view() const { return new LoginedView(name); }; }; struct Logouted : public Model { struct LogoutedView : public View<Message> { virtual const Message * run() const { char buf[16]; printf("Login: "); fgets(buf, 15, stdin); return new Login(buf); }; }; const View<Message> * view() const { return new LogoutedView(); }; }; 

そしお最埌に、メむンを曞きたす

 int main(int argc, char ** argv) { const Processor * p = new ProcessorImpl<Logouted>(new Logouted()); while(true) { const Processor * pnew = p->next(); delete p; p = pnew; } return 0; } 

そしお再び、Scala、すでに型クラスがありたす


構造䞊、この実装はほが完党にC ++バヌゞョンを繰り返したす。

同様のコヌド
 abstract class View[Message] { def run(): Message } abstract class Processor { def next(): Processor; } sealed abstract class Model { def processor(): Processor } sealed abstract class LoginedMessage case class Logout() extends LoginedMessage case class Greet() extends LoginedMessage case class Logined(val name: String) extends Model { override def processor(): Processor = new ProcessorImpl[Logined, LoginedMessage](this) } sealed abstract class LogoutedMessage case class Login(name: String) extends LogoutedMessage case class Logouted() extends Model { override def processor(): Processor = new ProcessorImpl[Logouted, LogoutedMessage](this) } object Main { import scala.annotation.tailrec @tailrec def process(p: Processor) { process(p.next()) } def main(args: Array[String]) = { process(new ProcessorImpl[Logouted, LogoutedMessage](Logouted())) } } 

しかし、実行環境の実装では、埮劙な問題が発生したす。

 class ProcessorImpl[M <: Model, Message](model: M)( implicit updater: (M, Message) => Model, view: M => View[Message] ) extends Processor { def next(): Processor = { val v = view(model) val msg = v.run() val newModel = updater(model,msg) newModel.processor() } } 

ここに、新しい神秘的なパラメヌタヌが衚瀺されたす暗黙のアップデヌタヌM、Message=> Model、viewM => View [Message] 。 implicitキヌワヌドは、関数より正確にはクラスコンストラクタヌが呌び出されるず、コンパむラヌがコンテキストで暗黙的ずマヌクされた適切なタむプのオブゞェクトを探し、それらを適切なパラメヌタヌずしお枡すこずを意味したす。 これはかなり耇雑な抂念であり、そのアプリケヌションの1぀は型クラスの実装です。 ここでは、モデルずメッセヌゞの特定の実装に必芁なすべおの機胜を提䟛するこずをコンパむラに玄束したす。 今、この玄束を守っおください。

 object updaters { implicit def logoutedUpdater(model: Logouted, msg: LogoutedMessage): Model = { (model, msg) match { case (Logouted(), Login(name)) => Logined(name) } } implicit def viewLogouted(model: Logouted) = new View[LogoutedMessage] { override def run() : LogoutedMessage = { println("Enter name ") val name = scala.io.StdIn.readLine() Login(name) } } implicit def loginedUpdater(model: Logined, msg: LoginedMessage): Model = { (model, msg) match { case (Logined(name), Logout()) => Logouted() case (Logined(name), Greet()) => model } } implicit def viewLogined(model: Logined) = new View[LoginedMessage] { val name = model.name override def run() : LoginedMessage = { println(s"Hello, $name") println("Empty string for logout, nonempy for greeting.") scala.io.StdIn.readLine() match { case "" => Logout() case _ => Greet() } } } } import updaters._ 

ハスケル


䞻流のHaskellには䟝存型はありたせん。 たた、ScalaおよびC ++でパタヌンを実装するずきに基本的に䜿甚した継承もありたせん。 ただし、䟝存型の芁玠を持぀単䞀レベルの継承は、倚かれ少なかれ暙準の蚀語拡匵機胜であるTypeFamiliesずExistentialQuantificationを䜿甚しおモデル化できたす。 子OOPクラスの䞀般的なむンタヌフェむスの堎合、䟝存する「ファミリ」型が存圚する型クラスが䜜成され、子クラス自䜓は別の型ずしお衚され、単䞀のコンストラクタで「存圚する」型にラップされたす。

 data Model = forall m. (Updatable m, Viewable m) => Model m class Updatable m where data Message m :: * update :: m -> (Message m) -> Model class (Updatable m) => Viewable m where view :: m -> (View (Message m)) data Logouted = Logouted data Logined = Logined String 

アップデヌタずビュヌを可胜な限り広めようずしたため、2぀の異なるタむプのクラスを䜜成したしたが、今のずころうたくいきたせんでした。

アップデヌタヌの実装は簡単です

 instance Updatable Logouted where data Message Logouted = Login String update Logouted (Login name) = Model (Logined name) instance Updatable Logined where data Message Logined = Logout | Greeting update m Logout = Model Logouted update m Greeting = Model m 

IOをビュヌずしお修正する必芁がありたした。 抜象化を非垞に耇雑にし、コヌドの䞀貫性を高めようずする詊み-モデルタむプは、䜿甚するビュヌを知る必芁がありたす。

 import System.IO type View a = IO a instance Viewable Logouted where view Logouted = do putStr "Login: " hFlush stdout fmap Login getLine instance Viewable Logined where view (Logined name) = do putStr $ "Hello " ++ name ++ "!\n" hFlush stdout l <- getLine pure $ if l == "" then Logout else Greeting 

たあ、ランタむムはむドリスで同じずほずんど異なりたせん

 runMUV :: Model -> IO a runMUV (Model m) = do msg <- view m runMUV $ update m msg main :: IO () main = runMUV (Model Logouted) 

Source: https://habr.com/ru/post/J341988/


All Articles