型クラス、モナド

今日の記事のトピックは、型クラス、いくつかの標準クラス、それらを使用した構文糖、およびモナドのクラスです。
クラスは、従来の命令型言語のインターフェースと同様に動的な多相性を導入し、Haskellにない関数のオーバーロードの代替として使用することもできます。
型クラス、そのインスタンス、およびすべてが内部でどのように機能するかを定義する方法を説明します。

前の記事:
データ型、パターンマッチング、および機能
基本

型クラス


データ型を作成し、そのための比較演算子を作成するとします。 問題は、Haskellには関数のオーバーロードがないため、この目的で(==)を簡単な方法で使用しても機能しないことです。 さらに、各タイプに新しい名前を付けることはオプションではありません。
ただし、比較する型のクラスを定義できます。 次に、データ型がこのクラスに属している場合、この型の値を比較できます。
class MyEq a where
myEqual :: a -> a -> Bool
myNotEqual :: a -> a -> Bool
MyEqは型クラスの名前(データ型など、大文字で始まる必要があります)、 aはこのクラスに属する型です。 クラスには複数のFooClass abcFooClass abc )がありますが、この場合は1つだけです。
対応する関数が定義されている場合、タイプはMyEqクラスに属します。
クラスでは、デフォルトの関数を定義できます。 たとえば、関数myEqualmyNotEqualは相互に表現できます。
myEqual xy = not ( myNotEqual xy )
myNotEqual xy = not ( myEqual xy )
このような定義は無限の再帰につながりますが、クラスインスタンスでは少なくとも1つを定義すれば十分です。
Boolクラスインスタンスを作成します。
instance MyEq Bool where
myEqual True True = True
myEqual False False = True
myEqual _ _ = False
インスタンス定義は、 instanceキーワードで始まり、クラス自体の定義にあるタイプa変数の代わりに、インスタンスが定義されているタイプ、つまり Bool
1つの関数myEqualを定義しmyEqual 、インタープリターで結果を確認できます。
ghci> myEqual True True
True
ghci> myNotEqual True False
True
ghci> :t myEqual
myEqual :: (MyEq a) => a -> a -> Bool
myEqual関数の型は、型に制約を課していることがmyEqualます— MyEqクラスに属している必要があります。
クラス自体を宣言するときに、同じ制限を課すことができます。
class ( MyEq a ) => SomeClass a where
-- ...

クラスはインターフェイスに多少似ています-関数を宣言し、そのための実装を提供します。 他の関数がこれらの関数を使用できるのは、あるタイプについてそのような実装がある場合のみです。
ただし、機能とメカニズム自体の実装の両方に大きな違いがあります。
  1. myEqual関数からmyEqualように、タイプa 2つの値を取りますが、仮想関数はthisパラメーターを1つだけ受け取ります。
    instance MyEq Boolおよびinstance MyEq Intがあるinstance MyEq Boolでも、 myEqual True 5関数の呼び出しは失敗します。
    ghci> myEqual True (5::Int)

    <interactive>:1:14:
    Couldn't match expected type `Bool' against inferred type `Int'
    In the second argument of `myEqual', namely `(5::Int)'
    In the expression: myEqual True (5 :: Int)
    In the definition of `it': it = myEqual True (5 :: Int)
    ghci>
    コンパイラー(インタープリター)は、パラメーターmyEqualが同じタイプでなければならないことを知っているため、そのような試みを防ぎます。
  2. クラスインスタンスはいつでも定義でき、これも非常に便利です。 インターフェースからの継承は、クラス自体を定義するときに示されます。
  3. 関数では、データ型が複数のクラスに一度に属することが必要になる場合があります。
    foo :: ( Eq a , Show a , Read a ) => a -> String -> String
    たとえば、C ++ / C#でこれを行う方法は? 複合IEquableShowableReadable作成しますか? しかし、あなたは彼から相続しないでしょう。 引数を3回渡して、異なるインターフェースを導き、関数内でこれが同じオブジェクトであり、呼び出し側に責任があると仮定しますか?

すでにC ++に言及しているので、同時に、新しい標準C ++ 0x コンセプトとconcept_mapには型クラスの本質がありますが、コンパイル段階で使用されます:)

しかし、欠点があります。 たとえば、 Showクラス(関数show :: a -> String )に属するオブジェクトのリストを取得するにはどうすればよいですか?
方法はありますが、非標準です。 適切なGHCオプションをファイルの先頭に追加する必要があります。
{-#
OPTIONS_GHC
-XExistentialQuantification
#-}

-- , , TAB' GHC
これで、このデータ型を宣言できます。
data ShowObj = forall a . Show a => ShowObj a
存在タイプについての詳細を読む)
同時に彼のためにShowクラスのインスタンスを定義し、彼自身も彼に所属するようにします。
instance Show ShowObj where
show ( ShowObj x ) = show x
チェック:
ghci> [ShowObj 4, ShowObj True, ShowObj (ShowObj 'x')]
[4,True,'x']
明示的にshow関数を呼び出したわけではありませんが、インタープリターはそれを使用するため、すべてが機能することを確認できます。

これはどのように実装されていますか?


隠されたパラメーターは、型に制限を課す関数に渡されます(各クラスと型には独自の値があります)-すべての必要な関数を備えた辞書。
実際、 instanceは特定のタイプの辞書インスタンスの定義です。
わかりやすくするために、HaskellおよびC ++のEq擬似実装を示します。
-- class MyEq
data MyEq a = MyEq {
myEqual :: a -> a -> Bool ,
myNotEqual :: a -> a -> Bool }

-- instance MyEq Bool
myEqBool = MyEq {
myEqual = \ xy -> x == y ,
myNotEqual = \ xy -> not ( x == y ) }

-- foo :: (MyEq a) => a -> a -> Bool
foo :: MyEq a -> a -> a -> Bool
foo dict xy = ( myEqual dict ) xy
-- foo True False
fooResult = foo myEqBool True False
C ++でも同じこと:
#include <iostream> <br/>
<br/>
// class MyEq a <br/>
class MyEq<br/>
{ <br/>
public : <br/>
virtual ~MyEq ( ) throw ( ) { } <br/>
// void const *, <br/>
virtual bool unsafeMyEqual ( void const * x, void const * y ) const = 0 ;<br/>
virtual bool unsafeMyNotEqual ( void const * x, void const * y ) const = 0 ;<br/>
} ;<br/>
<br/>
// , <br/>
// <br/>
template < typename T > <br/>
class MyEqDictBase : public MyEq<br/>
{ <br/>
virtual bool unsafeMyEqual ( void const * x, void const * y ) const <br/>
{ return myEqual ( * static_cast < T const * > ( x ) , * static_cast < T const * > ( y ) ) ; } <br/>
virtual bool unsafeMyNotEqual ( void const * x, void const * y ) const <br/>
{ return myNotEqual ( * static_cast < T const * > ( x ) , * static_cast < T const * > ( y ) ) ; } <br/>
public : <br/>
virtual bool myEqual ( T const & x, T const & y ) const { return ! myNotEqual ( x, y ) ; } <br/>
virtual bool myNotEqual ( T const & x, T const & y ) const { return ! myEqual ( x, y ) ; } <br/>
} ;<br/>
<br/>
// . . <br/>
template < typename T > <br/>
class MyEqDict;<br/>
<br/>
// <br/>
template < typename T > <br/>
MyEqDict < T > makeMyEqDict ( ) { return MyEqDict < T > ( ) ; } <br/>
<br/>
// instance MyEq Bool <br/>
// MyEq bool <br/>
template <> <br/>
class MyEqDict < bool > : public MyEqDictBase < bool > <br/>
{ <br/>
public : <br/>
virtual bool myEqual ( bool const & l, bool const & r ) const { return l == r; } <br/>
} ;<br/>
<br/>
// <br/>
// , bool, Haskell' <br/>
bool fooDict ( MyEq const & dict, void const * x, void const * y ) <br/>
{ <br/>
return dict. unsafeMyNotEqual ( x, y ) ; // myNotEqual <br/>
} <br/>
<br/>
// <br/>
// foo :: (MyEq a) => a -> a -> Bool <br/>
template < typename T > <br/>
bool foo ( T const & x, T const & y ) <br/>
{ <br/>
return fooDict ( makeMyEqDict < T > ( ) , & x, & y ) ;<br/>
} <br/>
<br/>
int main ( ) <br/>
{ <br/>
std :: cout << foo ( true , false ) << std :: endl ; // 1 <br/>
std :: cout << foo ( false , false ) << std :: endl ; // 0 <br/>
return 0 ;<br/>
}

いくつかの標準クラスと構文糖


Enumは列挙です。 次/前の値、および数値による値を取得するための関数を定義します。
リストの構文シュガーで使用される[ 1 .. 10 ] 、実際には、 enumFromTo 1 10[ 1 , 3 .. 10 ] => enumFromThenTo 1 3 10

表示 -文字列への変換、 show :: a -> Stringの主な機能。
たとえば、インタープリターが値を出力するために使用します。

読み取り -文字列からの変換、メイン関数read :: String -> a
「汚れた」例外ではなく、「クリーン」な方法でエラーを通知するために、 Maybe a (オプションの値)ではなく、値を返すことを選択した理由はわかりません。

Eq-比較、演算子( == )および( /= ) (等号を消したような)

順序付き型、演算子( < ) ( > ) ( <= ) ( >= ) 。 クラスEqの型メンバーシップが必要です。

さまざまなクラスのより完全なリストは、 ここにあります

ファンクター-ファンクター、 fmap :: ( a -> b ) -> fa -> fb :(a- fmap :: ( a -> b ) -> fa -> fb関数。
明確にするために、この関数の使用例を示します。
ghci> fmap (+1) [1,2,3]
[2,3,4]
ghci> fmap (+1) (Just 6)
Just 7
ghci> fmap (+1) Nothing
Nothing
ghci> fmap reverse getLine
hello
"olleh"
つまり リストの場合は単なるmap 、オプションの値の場合関数( + 1 )が値自体である場合に適用され、入出力の場合はこの入出力の結果に関数が適用されます
I / Oについては以下でgetLineしますが、 getLineは文字列を返さないため、 reverseを直接適用することはできません。

毎回新しく書き込まれたデータ型のインスタンスを指定することを避けるために、Haskellはいくつかのクラスに対してこれを自動的に行うことができます。
data Test a = NoValue | Test aa deriving ( Show , Read , Eq , Ord )
data Color = Red | Green | Yellow | Blue | Black | Write deriving ( Show , Read , Enum , Eq , Ord )
ghci> NoValue == ( Test 4 5 )
False
ghci> read "Test 5 6" :: Test Int
Test 5 6
ghci> ( Test 1 100 ) < ( Test 2 0 )
True
ghci> ( Test 2 100 ) < ( Test 2 0 )
False
ghci> [ Red .. Black ]
[ Red , Green , Yellow , Blue , Black ]

偉大で恐ろしいモナド


Monadクラスは「計算」です。つまり、 計算と結果を組み合わせる方法を説明できます。
すでに書かれているものよりも良い記事を書く可能性は低いので、これらのモナドが必要な理由を理解するために、最高の(私の意見では)記事へのリンクを提供します。
1. モナド -ロシア語のモナドに関するSPbHUGの記事と、おなじみの命令型言語での類推
2. IO inside-モナドを使用した入出力に関する英語の記事
3. さらにもう1つのHaskellチュートリアル -Haskellに関する本、 モナドのセクションでは 、モナドの本質であるComputationクラスの作成例について非常によく書かれています。

ここでは、後でのぞき見できるように非常に簡単に説明します。
次のそれぞれが前の計算の結果に依存する逐次的な計算を記述したいとします(何らかの不特定の方法で)。 これを行うには、適切なクラスを定義できます。このクラスには、少なくとも2つの関数が必要です。
class Computation c where
return :: a -> ca
bind :: ca -> ( a -> cb ) -> cb
最初の関数は基本的に何らかの値を取り、実行されると単純に同じ値を返す計算を返します。
2番目の関数は2つの引数を取ります。
1.実行されると、タイプa値を返す計算
2.タイプaの値を取り、タイプb値を返す新しい計算を返す関数
結果は、タイプb値を返す計算です。

なぜこれがすべて必要なのか、オプションの値の例を示します
data Maybe a = Just a | Nothing
いくつかの関数があり、それぞれが失敗してNothingを返す場合があるとします。 次に、それらを直接使用すると、そのような読みにくいコードを取得する危険があります。
funOnMaybes x =
case functionMayFail1 x of
Nothing -> Nothing -- ,
Just x1 -> -- , ,
case functionMayFail2 x1 of
Nothing -> Nothing -- ( "")
Just x2 -> --
つまり これらの関数を単に順番に呼び出すのではなく、毎回値を確認する必要があります。
しかし、この言語では、関数を引数として渡し、同じ方法で戻ることができます。
combineMaybe :: Maybe a -> ( a -> Maybe b ) -> Maybe b
combineMaybe Nothing _ = Nothing
combineMaybe ( Just x ) f = fx
combineMaybe関数は、オプションの値と関数を受け入れ、この関数をオプションの値に適用した結果、または失敗を返します。
funOnMaybes関数を書き換えます:
funOnMaybes x = combineMaybe ( combineMaybe x functionMayFail1 ) functionMayFail2 --...
または、関数をインフィックスと呼ぶことができるという事実を使用して:
funOnMaybes x = x `combineMaybe` functionMayFail1 `combineMaybe` functionMayFail2 --...

関数combineMaybeのタイプがbindのタイプを正確に繰り返していることに気付くでしょうcombineMaybe代わりにMaybeだけMaybeありc
instance Computation Maybe where
return x = Just x
bind Nothing f = Nothing
bind ( Just x ) f = fx
それがまさにモナドの定義Maybeません。そこではbindだけが呼び出され( >>= ) 、さらに前の計算の結果を使用しない追加の演算子( >> )があります。
さらに、モナドには構文糖が定義されているため、モナドの使用が大幅に簡素化されます。
funOnMaybes x = do
x1 <- functionMayFail1 x
x2 <- functionMayFail2 x1
if x2 == (0)
then return (0)
else do
x3 <- functionMayFail3 x2
return ( x1 + x3 )
else内部の余分なdoとその時の不在に注意してdodoは、複数の計算を1つに結合する単なる構文糖であり、 thenブランチ( return (0) )には1つの計算があるため、そこでは必要ありません。 elseブランチでは、計算は連続して2つあり、それらを結合するには、再びdo使用する必要がありdo
戻る矢印(<-)した特別な構文は、非常に簡単に変換されます。
1. {e}-> eを実行します
1つの計算でこの計算自体があり、結合する必要はありません
2. {e; es}-> e >> do {es}
複数の連続した計算が演算子によって接続されています( >> )
3. {宣言を許可する; es}-> do {es}で宣言を行う
doの中では、 let ... inような追加の宣言を行うことができます
4. {p <-e; es}-> ok p = do {es}; ok _ = eの「...」に失敗します>> = ok
最初の計算の結果が後で使用される場合、演算子( >>= )組み合わせに使用されます
後者の場合、 pはサンプルであり、一致しない可能性があるため、このような構造が使用されます。
一致する場合、さらに計算が実行されます。一致しない場合は、文字列の説明を含むエラーが返されます。
fail関数は、一般的にモナドの概念とは関係のない、モナドの別の追加関数です。

標準ライブラリには他にどのようなモナドインスタンスがありますか?

状態 -状態の計算。 State内部デバイスを認識するget/put関数を使用して、状態を取得および設定できます。
import Control . Monad . State

fact' :: Int -> State Int Int -- - Int, - Int
fact' 0 = do
acc <- get --
return acc --
fact' n = do
acc <- get --
put ( acc * n ) -- n
fact' ( n - 1 ) --

fact :: Int -> Int
fact n = fst $ runState ( fact' n ) 1 -- - 1
runStateは、状態を持つ関数を計算し、結果と変更された状態を含むタプルを返します。 結果のみが必要なので、 fstです。

リストもモナドです。 次の計算は、前の結果のすべてに適用されます。
dummyFun contents = do
l <- lines contents --
if length l < 3 then fail "" -- 3
else do
w <- words l --
return w --
ghci> dummyFun "line1\nword1 word2 word3\n \n \nline5"
["line1","word1","word2","word3","line5"]


Haskellの継続-モナド-Cont
継続は、 ロシアのwiki英語のwikiで読むことができ、リンクをたどります。
それらは特別な注意に値しますが、私はすべてに適合するわけではなく、モナドとは直接関係がありません。
Schemeの継続に関する優れた記事は、ライブログでpalm_muteユーザーから入手できます。

I / Oもモナドを使用して実装されます。 たとえば、 getLine関数のタイプは次のgetLineです。
getLine :: IO String
文字列を返すIOアクション。 IOは次のように理解できます。
getLine :: World -> ( String , World )
ここで、 WorldWorldの状態です
つまり 入出力関数は、いわば世界の状態を取得し、特定の結果と世界の新しい状態を返します。これはその後の関数で使用されます。
もちろん、これは単なる精神的な表現です。
リストやたぶんとは異なり、タイプIOコンストラクターがないため、タイプIO String結果をコンポーネントに解析することはできませんが、他の計算で使用するだけで、入出力の使用が確実に反映されます。関数のタイプ。

(「決して」-大声で言われますが、実際にはunsafePerformIO :: IO a -> aがありますが、問題を理解し、絶対に必要な場合にのみ使用するのは安全ではありません。

言及する価値があるのはMonadトランスフォーマーです。
状態が必要な場合、I / OがIOであればStateを使用できます。 しかし、関数がステートを持ちながらI / Oを実行したい場合はどうでしょうか?
この目的のために、 StateTトランスフォーマーが設計されており、 Stateと別のモナドを接続します。 この他のモナドで計算を実行するには、 lift関数が使用されます。
階乗の例を見てみましょう。これはバッテリーを印刷するように変更します
import Control . Monad . State

fact' :: Int -> StateT Int IO Int -- IO
fact' 0 = do
acc <- get
return acc
fact' n = do
acc <- get
lift $ print acc -- print acc - IO (), lift
put ( acc * n )
fact' ( n - 1 )

fact :: Int -> IO Int -- IO , :)
fact n = do
( r , _ ) <- runStateT ( fact' n ) 1
return r
ghci> fact 5
1
5
20
60
120
120

StateTに加えて ListT (リスト用)もあります。

モナドトランスフォーマーモナドの完全なリスト。

便宜上、一般化された関数はモナド上で定義されます。 彼らの名前は自明であり、それらのほとんどはリスト関数を複製しているので、それらの一部をリストし、 このリンクを提供します
sequence :: Monad m => [ ma ] -> m [ a ]
--
mapM f = sequence . map f
forM = flip mapM
-- forM [1..10] $ \i -> print i
forever :: Monad m => ma -> mb -- ,
filterM :: Monad m => ( a -> m Bool ) -> [ a ] -> m [ a ] -- filter
foldM :: Monad m => ( a -> b -> ma ) -> a -> [ b ] -> ma -- foldl
when :: Monad m => Bool -> m () -> m () -- if else

リスト内包表記


(私は推定しないこの用語を翻訳する)
リスト内包表記を使用すると、数学表記に従ってリストを設計できます。

ghci> take 5 $ [ 2 * x | x <- [ 1 .. ] , x ^ 2 > 3 ]
[ 4 , 6 , 8 , 10 , 12 ]
ghci> [ ( x , y ) | x <- [ 1 .. 3 ] , y <- [ 1 .. 3 ] ]
[ ( 1 , 1 ) , ( 1 , 2 ) , ( 1 , 3 ) , ( 2 , 1 ) , ( 2 , 2 ) , ( 2 , 3 ) , ( 3 , 1 ) , ( 3 , 2 ) , ( 3 , 3 ) ]
最後のリストがより頻繁に移動されることがわかります。
yはxに依存する場合があります。
ghci> [ x + y | x <- [ 1 .. 4 ] , y <- [ 1 .. x ] , even x ]
[ 3 , 4 , 5 , 6 , 7 , 8 ]

リストの内包表記も構文糖衣であり、次のように展開されます(最後の例)。
do
x <- [ 1 .. 4 ]
y <- [ 1 .. x ]
True <- return ( even x )
return ( x + y )
もちろん、それは直接展開されますが、リスト上のモナド計算とリスト内包表記との関係は非常に明白です。

次回はすべての質問に答えようとしますが、チャットの実装を取り上げることができます(または、質問がほとんどない場合はすぐに)、すべての点で明確ではないかどうかを尋ねます。

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


All Articles