メインコンテンツへ移動

型_Haskell ノート 3

無料2018-05-04#Functional_Programming#Haskell type#Haskell自定义类型#Haskell内置类型#Haskell类型派生

Haskell は静的型付けで、すべてに型があり、コンパイル時に確定する必要がある

一.内置タイプ

いくつかの一般的なタイプは以下の通り:

  • Int:有界整数、32 ビットマシン上の境界は [-2147483648, 2147483647]

  • Integer:無界整数、内置の巨大数タイプ、効率は Int より高くない

  • Float:単精度浮動小数点数、6 桁小数

  • Double:倍精度浮動小数点数、15 桁小数

  • Bool:ブール値、値は True/False

  • Char:文字

  • Tuple:タプル自体もタイプで、() という 1 つの値のみ

内置の無界整数により巨大数演算が非常に便利になります。例えば 100 の階乗を求める:

> product [1..100]
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

二.変数タイプ

name :: String
name = "ayqy"

:: は「タイプは」と読み(has type)、コンパイラに変数 nameString タイプ(つまり [Char] タイプ)であることを伝えます

また、タイプの最初の文字は大文字です

P.S. 理論的には多くのシーンで手動でタイプを明記する必要はありません(コンパイラが自分で推論します)が、実践的なアドバイスは少なくともトップレベル変数/関数にタイプを明記することです。もちろん、すべてに明記するのは確かに良い習慣です。明確なタイプは可読性を大幅に向上させるからです。詳細は Type signatures as good style を参照

P.S.:browse <module> コマンドで指定モジュールのすべてのタイプ推論をリストでき、既存コードにタイプを補充しやすくなります

三.関数タイプ

いくつかの一般的な関数のタイプシグネチャは以下の通り:

show :: Show a => a -> String
odd :: Integral a => a -> Bool
fromIntegral :: (Num b, Integral a) => a -> b
(+) :: Num a => a -> a -> a
(++) :: [a] -> [a] -> [a]

ここで、:: から => までの部分はタイプ制約(タイプ変数の宣言)で、=> 後の部分がそのタイプです。タイプ宣言中の小文字(例えば a)はタイプ変数と呼ばれ、制限されていないタイプ変数(++ タイプ中の a など)はジェネリックに相当し、タイプ変数を使用する関数は多相関数と呼ばれます

例えば show :: Show a => a -> String の意味は show のタイプは Show タイプパラメータを受け取り String を返す関数です。(+) :: Num a => a -> a -> a+ のタイプは 2 つの Num タイプパラメータを受け取り Num を返す(カリー化された)関数です。そして (++) :: [a] -> [a] -> [a]++ のタイプは 2 つの List パラメータを受け取り別の List を返す関数で、ここでの a は制限されていないため、List 内の要素は任意のタイプになれます

タイプ部分の -> は「へマップする」(maps to)と読みます。どのように理解すればよいでしょうか?

関数の数学的定義は定義域から値域へのマッピング関係です。したがって f = x -> y に対応する数学的意味は y = f(x) です。つまり xy へマップする(マッピング関係)のが f で、x を入力して対応する y を返します

したがって a -> b -> ca を入力し、関数 b -> c を返す関数を表し、この返された関数を続けて呼び出し、b を入力して対応する c を返します。カリー化特性を無視すれば、2 つのパラメータ a, b を受け取り c を返すと単純に理解できます

四.Typeclass

(==) :: Eq a => a -> a -> Bool

ここで、Eqtypeclass と呼ばれ、interface に相当し、つまり該タイプメンバーが持つべき動作を定義します

関数以外のすべてのタイプは Eq に属し、すべて等値性を判断できます。他の一般的な typeclass は以下の通り:

  • Ord:大小比較可能(<, >, <=, >= などの関数で大小比較できるため、Ord は必ず Eq に属します)

  • Show:文字列で表現可能(関数以外はすべて Show 可能)。show 関数で他のタイプを文字列に変換できます

  • Read:Show と逆。read 関数で文字列を他のタイプに変換できます

  • Enum:列挙可能、つまり連続的。()BoolCharOrderingIntIntegerFloatDouble を含み、これらのタイプはすべて Range に使用でき、succpred 関数で該タイプ値の後継と前身にアクセスできます

  • Bounded:明確な上下界あり。maxBoundminBound で指定タイプの上下界を取得できます(例:maxBound :: Int

  • Num:数値。メンバーはすべて数値の特徴を持ちます

  • Integral:整数。IntInteger を含む

  • Floating:小数。FloatDouble を含む

数値変換の場合、大范围から小范围へは暗黙的に完了できます(例:Num から Float へ)、小から大へは fromIntegral :: (Num b, Integral a) => a -> b などの関数で完了する必要があります。一般的なシーンは length 関数です:

> length "Hello" + 0.5

<interactive>:191:18: error:
    ? No instance for (Fractional Int) arising from the literal '0.5'
    ? In the second argument of '(+)', namely '0.5'
      In the expression: length "Hello" + 0.5
      In an equation for 'it': it = length "Hello" + 0.5

length :: Foldable t => t a -> Int のため、IntFractional は直接加算できないため、このようにする必要があります:

> (fromIntegral (length "Hello")) + 0.5
5.5

また、read 関数も非常に面白いです。例えば:

> read "12" + 4
16
> 1 : read "[2, 4]"
[1,2,4]

コンテキストから目標タイプを推論します。したがってコンテキストがないと推論できません:

> read "12"
*** Exception: Prelude.read: no parse

コンパイラは私たちが何を望んでいるのかわかりません。手動でタイプを宣言してヒントを与えられます:

> read "12" :: Int
12
> read "12" :: Float
12.0

五.カスタムタイプ

代数的データタイプ

Algebraic Data Type は、代数的演算で構築されたデータ構造を指します。ここで代数的演算には 2 種類あります:

  • sum:論理和。例えば Maybe タイプの可能値の間は論理和関係

  • product:論理積。例えばタプル成分の間は論理積の関係

例えば:

-- 論理積、Pair タイプは Int-Double ペア
data Pair = P Int Double
-- 論理和、Pair タイプは数値、Int または Double のどちらか
data Pair = I Int | D Double

論理和と論理積で任意に複雑なデータ構造を構築でき、すべて代数的データタイプと呼べます

地位から見ると、代数的データタイプの関数型言語における位置は、数学における代数のようであり、非常に基礎的なものです。同様に、代数的演算を行うには、まず数の定義が必要です:

[caption id="attachment_1705" align="alignnone" width="625"]map algebraic data type to math map algebraic data type to math[/caption]

宣言

data キーワードでカスタムタイプを宣言します:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

Shape タイプには 2 つの値コンストラクタ(Circle, Rectangle)があることを表します。つまり Shape タイプの値は Circle または Rectangle です。値コンストラクタの本質は関数 です:

Circle :: Float -> Float -> Float -> Shape
Rectangle :: Float -> Float -> Float -> Float -> Shape

値コンストラクタのパラメータ(例えば CircleFloat Float Float)はフィールド(field)とも呼ばれ、実際にはパラメータです

値コンストラクタは関数なので、パターンマッチはカスタムタイプにも使用できます:

circleArea (Circle _ _ r) = pi * r ^ 2
> circleArea (Circle 1 1 1)
3.1415927

面積を求める関数のタイプは:

circleArea :: Shape -> Float

パラメータタイプは Shape で、Circle ではありません。後者は値コンストラクタに過ぎず、タイプではないからです

また、パターンマッチはすべて値コンストラクタ针对です。一般的な [], otherwise/True, 5 などはすべてパラメータなし値コンストラクタです

再帰定義タイプ

あるタイプの値コンストラクタのパラメータ(field)が該タイプである場合、再帰定義が発生します

例えば List の構文糖:

[1, 2, 3]
-- 等价于(:右結合、括弧は必須ではない)
1 : (2 : (3 : []))

再帰定義の一種です:List は先頭項を残りの項で構成される List の左側に挿入します

手動で作ってみましょう:

infixr 5 :>
data MyList a = MyEmptyList | a :> (MyList a) deriving (Show)

ここで、カスタム演算子 :>: に相当し、どちらも値コンストラクタに属します(したがって x:xs のパターンマッチは実際には List の値コンストラクタ : 针对です)。試してみましょう:

> :t MyEmptyList
MyEmptyList :: MyList a
> 3 :> 5 :> MyEmptyList
3 :> (5 :> MyEmptyList)
> :t 3 :> 5 :> MyEmptyList
3 :> 5 :> MyEmptyList :: Num a => MyList a

構文上の違いを除き、List 定義(3 : 5 : [])と基本的に一致します。List 特色関数をいくつか作ってみましょう:

_fromList [] = MyEmptyList
_fromList (x:xs) = x :> (_fromList xs)
_map f MyEmptyList = MyEmptyList
_map f (x :> xs) = f x :> _map f xs

続けて試してみましょう:

> _fromList [1, 2, 3]
1 :> (2 :> (3 :> MyEmptyList))
> _map (+ 1) (_fromList [1, 2, 3])
2 :> (3 :> (4 :> MyEmptyList))

派生

Show クラス(typeclass)のメンバーのみが GHCi 環境で直接出力できます(出力前に show :: Show a => a -> String を呼び出すため)。したがって、ShapeShow のメンバーにします:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

deriving キーワードでタイプ派生を宣言し、あるタイプの値を他のタイプのメンバーにします。Shape 値を直接出力してみましょう:

> Circle 1 1 1
Circle 1.0 1.0 1.0

座標点も切り出してみましょう:

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
circleArea (Circle _ r) = pi * r ^ 2

Show の他に、自動的にデフォルト動作を追加できる他の typeclass は Eq, Ord, Enum, Bounded, Read です。例えば Eq から派生後、==/= で値の等値性を比較できます:

data Mytype = Mytype Int String deriving (Show, Eq)
> Mytype 3 "a" == Mytype 4 "b"
False
> Mytype 3 "a" == Mytype 3 "a"
True

実際、Eq から派生する際に自動的に追加される等値性判断は、入力パラメータが一致するかどうかをチェックします:

1. 値コンストラクタが一致するかどうかをチェック
2. 値コンストラクタのパラメータが一致するかどうかをチェック

もちろん、パラメータも Eq クラスメンバーである必要があります。そうでないと自動比較できません(満たさない場合、エラーがスローされます

ShowRead も同様で、文字列と値の間の相互変換を完了します:

data Mytype = Mytype Int String deriving (Show, Eq, Read)
> Mytype 3 "a"
Mytype 3 "a"
> read "Mytype 3 \"a\"" :: Mytype
Mytype 3 "a"

Ord は非常に面白く、メンバーがソート可能であることを表しますが、デフォルトのソート基準はどのように確定するのでしょうか?

data Mytype = EmptyValue | Singleton | Mytype Int String deriving (Show, Eq, Read, Ord)
> EmptyValue < Singleton
True
> Singleton < Mytype 3 "a"
True
> Mytype 3 "a" < Mytype 4 "a"
True

まずタイプ宣言中の順序を見ます。または(|)で一緒に、最初に出現する値コンストラクタが作成する値が最小で、その後同様のルールで値コンストラクタのパラメータを比較します。したがってパラメータもすべて Ord メンバーである必要があります

Enum, Bounded は列挙タイプ、つまり有限集合を定義するために使用されます。Enum は各値に前身/後継があることを要求し、これで Range に使用できます。Bounded は値に上下界があることを要求します。例えば:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Show, Bounded, Enum)
-- 上下界
> maxBound :: Day
Sunday
> minBound :: Day
Monday
-- 前身/後継
> pred Wednesday
Tuesday
> succ Wednesday
Thursday
-- Range
> [Wednesday ..]
[Wednesday,Thursday,Friday,Saturday,Sunday]

Record

シンプルなデータタイプ、例えば Vector2D の場合:

data Vector2D = Vector2D Float Float deriving(Show)

シンプルな data 定義でセマンティックニーズを満たせます(2 次元ベクトルの 2 つのパラメータが横縦座標であることを明確に知っています)。説明対象が複雑なものである場合、例えば人には年齢、身長、体重、スリーサイズがあります:

data Person = Person Float Float Float Float Float Float deriving(Show)

これは見ていて直感的ではありません。コメントを追加しましょう:

-- 年齢 身長 体重 上囲 中囲 下囲
data Person = Person Float Float Float Float Float Float deriving(Show)

何を思いつきましたか?これは 10 几个パラメータの関数ではありませんか!パラメータが非常に多く順序も要求され、さらに面倒なのは、これはデータタイプであり、一連の getter が必要です:

getAge (Person age _ _ _ _ _) = age
getHeight (Person _ height _ _ _ _) = height
-- ...など一堆の getter

他の言語では通常この状況をどのように処理しますか?散在するパラメータを整理します(例えばオブジェクトを作成):

data Person = Person {
  age :: Float,
  height :: Float,
  weight :: Float,
  xw :: Float,
  yw :: Float,
  tw :: Float
} deriving (Show)

person を作成し、セマンティックが明確で、パラメータ順序を気にする必要はありません:

person =  Person {age=1, height=2, xw=4, yw=5, tw=6, weight=3}

自動的に一堆の getter が作成されます。例えば:

> :t age
age :: Person -> Float
> weight person
3.0

単純なタイプ定義よりもはるかに便利です

タイプパラメータ

タイプコンストラクタはパラメータを受け取り、新しいタイプを返せます。例えば:

data Maybe a = Nothing | Just a

ここで、a はタイプパラメータで、Maybe はタイプではなく、タイプコンストラクタ です。具体的な Maybe xxx がタイプで、NothingJust xxx はすべて該タイプの値です。例えば:

Just 'a' :: Maybe Char
Nothing :: Maybe a

このようにして一連の動作が類似したタイプを得られます。应用场景から見ると、パラメータ付きタイプはジェネリックに相当 し、具体的タイプの上の 1 層の抽象です。例えば古典的な List

[1, 2, 3] :: Num t => [t]
"456" :: [Char]

はいくつかの動作をサポートします(Data.List モジュール定義の各種関数):

map :: (a -> b) -> [a] -> [b]
> map (+ 1) [1, 2, 3]
[2,3,4]
> map (Data.Char.chr . (+ 1) . Data.Char.ord) "456"
"567"

length :: Foldable t => t a -> Int
> length [1, 2, 3]
3
> length "456"
3

maplength 関数は List a の具体的タイプが何であるかを気にせず、抽象データタイプ上で定義された演算とみなせます

Maybe と Either

data Maybe a = Nothing | Just a 	-- Defined in 'GHC.Base'
data Either a b = Left a | Right b 	-- Defined in 'Data.Either'

应用场景では、Maybe はエラーが発生する可能性のある結果を表すために使用され、成功は Just a、失敗は Nothing です。単一エラー原因のシーンに適しています。例えば elemIndex

Data.List.elemIndex :: Eq a => a -> [a] -> Maybe Int

見つかった場合は Just Int タイプのインデックスを返し、見つからない場合は Nothing を返します。第 3 の結果はありません

例外処理のシーンのみを見ると、Either はより強力です。一般的に失敗原因を Left a に、成功結果を Right b に配置します。形式上は Maybe と非常に似ていますが、Left a は任意の情報を運べます。それに対して、Nothing はあまりにも曖昧です

P.S.JS コンテキストでは、Maybe は成功すれば値を返し、失敗すれば false を返すことに相当し、失敗したことはわかりますが、具体的な原因はわからない可能性があります。Either はコールバック関数の第 1 パラメータがエラー情報を運ぶことに相当し、空でなければ失敗で、具体的な原因是该パラメータの値です

タイプエイリアス

Type synonyms(タイプ同義語、つまりタイプエイリアス)は、すでに見たことがあります:

> :i String
type String = [Char] 	-- Defined in 'GHC.Base'

type キーワードでタイプにエイリアスを定義し、String[Char] と同等にし、これによりタイプ宣言にセマンティックな利点をもたらします。例えば:

type PhoneNumber = String
type Name = String
type PhoneBook = [(Name,PhoneNumber)]

inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook

姓名、電話、電話簿を入力し、電話簿に該記録があるかどうかを返します。エイリアスを付けない場合、タイプ宣言はこのようになります:

inPhoneBook :: String -> String -> [(String, String)] -> Bool

もちろん、このシーンは少し大げさに見えるかもしれません。実際的な作用がないもの(タイプ宣言)のためにこれほど多くのことを行うために。しかしタイプエイリアスの特性は、特定のシーン针对ではなく、タイプ定義のセマンティックをより形象的で生き生きとしたものにする能力を提供することです。例えば:

  • タイプ宣言をより読みやすくする

  • 重複率の高い長い名前のタイプを置き換える(例:[(String, String)]

この能力によりタイプが事物を記述することをより明確にできます

タイプエイリアスもパラメータを持てます。例えば、カスタム連想リスト:

type AssocList k v = [(k, v)]

任意の k-v を許可し、その汎用性を保証します。例えば:

inPhoneBook :: (Eq k, Eq v) => k -> v -> AssocList k v -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook
> inPhoneBook 1 "1234" [(0, "0012"), (1, "123")]
False

この時 AssocList k v に対応する具体的タイプは AssocList Int String です:

> read "[(0, \"0012\"), (1, \"123\")]" :: AssocList Int String
[(0,"0012"),(1,"123")]

タイプエイリアスもカリー化に類似した特性があります。例えば:

type IntAssocList = Int
-- 等价于、1 つのパラメータを保持
type IntAssocList v = Int v

パラメータが十分であれば具体的タイプで、そうでなければパラメータ付きタイプです

参考資料

コメント

コメントはまだありません

コメントを書く