일.내장 타입
몇 가지 일반적인 타입은 다음과 같습니다:
-
Int: 유계 정수, 32 비트 머신상의 경계는[-2147483648, 2147483647] -
Integer: 무계 정수, 내장된 대수 타입, 효율은Int보다 높지 않음 -
Float: 단정밀도 부동소수점, 6 자리 소수 -
Double: 배정밀도 부동소수점, 15 자리 소수 -
Bool: 불리언 값, 값은True/False -
Char: 문자 -
Tuple: 튜플 자체도 타입이며,()하나의 값만 있음
내장된 무계 정수로 인해대수 연산이 매우 편리해집니다. 예를 들어 100 의 계승을 구함:
> product [1..100]
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
이.변수 타입
name :: String
name = "ayqy"
:: 는 "타입은"으로 읽으며 (has type), 컴파일러에 변수 name 이 String 타입 (즉 [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 는 + 의 타입은 두 개의 Num 타입 파라미터를 받아 Num 을 반환하는 (커리화된) 함수입니다. 그리고 (++) :: [a] -> [a] -> [a] 는 ++ 의 타입은 두 개의 List 파라미터를 받아 다른 List 를 반환하는 함수이며, 여기의 a 는 제한되지 않았으므로 List 내의 요소는 임의의 타입이 될 수 있습니다
타입 부분의 -> 는 "로 매핑" (maps to) 으로 읽습니다. 어떻게 이해해야 할까요?
함수의 수학적 정의는정의역에서 치역으로의 매핑 관계입니다. 따라서 f = x -> y 에 대응하는 수학적 의미는 y = f(x) 입니다. 즉 x 가 y 로 매핑하는 (매핑 관계) 것이 f 로, x 를 입력하여 대응하는 y 를 반환합니다
따라서 a -> b -> c 는 a 를 입력하여 함수 b -> c 를 반환하는 함수를 나타내며, 이 반환된 함수를 계속 호출하여 b 를 입력하여 대응하는 c 를 반환합니다. 커리화 특성을 무시하면, 두 개의 파라미터 a, b 를 받아 c 를 반환한다고 단순하게 이해할 수 있습니다
사.Typeclass
(==) :: Eq a => a -> a -> Bool
여기서 Eq 는typeclass 라고 하며, interface 에 상당하며, 즉 해당 타입 멤버가 가져야 할 동작을 정의합니다
함수를 제외한 모든 타입은 Eq 에 속하며, 모두 등가성을 판단할 수 있습니다. 다른 일반적인 typeclass 는 다음과 같습니다:
-
Ord: 크기 비교 가능 (<, >, <=, >=등의 함수로 크기 비교할 수 있으므로Ord는 반드시Eq에 속함) -
Show: 문자열로 표현 가능 (함수 외는 모두 Show 가능).show함수로 다른 타입을 문자열로 변환할 수 있음 -
Read: Show 와 반대.read함수로 문자열을 다른 타입으로 변환할 수 있음 -
Enum: 열거 가능, 즉 연속적.(),Bool,Char,Ordering,Int,Integer,Float,Double을 포함하며, 이 타입들은 모두 Range 에 사용할 수 있고,succ와pred함수로 해당 타입 값의 후계와 전임에 액세스할 수 있음 -
Bounded: 명확한 상하한 있음.maxBound와minBound로 지정 타입의 상하한을 취할 수 있음 (예:maxBound :: Int) -
Num: 수치. 멤버는 모두 수치의 특징을 가짐 -
Integral: 정수.Int와Integer포함 -
Floating: 소수.Float와Double포함
수치 변환의 경우, 대범위에서 소범위로 암시적으로 완료할 수 있으며 (예: 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 이므로, Int 와 Fractional 은 직접 더할 수 없어 이렇게 해야 합니다:
> (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 은 대수적 연산으로 구축된 데이터 구조를 가리킵니다. 여기서 대수적 연산에는 두 가지가 있습니다:
-
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[/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
값 생성자의 파라미터 (예: Circle 의 Float 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 를 호출하므로). 따라서 Shape 를 Show 의 멤버로 만듭니다:
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 클래스 멤버여야 합니다. 그렇지 않으면 자동 비교할 수 없습니다 (만족하지 않으면 에러가 스로우됩니다)
Show 와 Read 도 유사하며, 문자열과 값 사이의 상호 변환을 완료합니다:
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 차원 벡터의 두 파라미터가 횡종좌표임을 명확히 압니다). 설명 대상이 복잡한 것인 경우, 예를 들어 사람에게는 연령, 신장, 체중, 쓰리사이즈가 있습니다:
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 가 타입이며, Nothing 과 Just xxx 는 모두 해당 타입의 값입니다. 예를 들어:
Just 'a' :: Maybe Char
Nothing :: Maybe a
이렇게 하여 일련의 동작이 유사한 타입을 얻을 수 있습니다. 응용 장면에서 보면, 파라미터가 있는 타입은 제네릭에 상당 하며, 구체적 타입 위의 한 층의 추상입니다. 예를 들어 고전적인 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
map 과 length 함수는 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 을 반환합니다. 세 번째 결과는 없습니다
예외 처리 장면만 보면 Either 가 더 강력합니다. 일반적으로 실패 원인을 Left a 에, 성공 결과를 Right b 에 배치합니다. 형태상으로는 Maybe 와 매우 유사하지만 Left a 는 임의의 정보를 운반할 수 있습니다. 이에 비해 Nothing 은 너무 모호합니다
P.S.JS 컨텍스트에서 Maybe 는 성공하면 값을 반환하고 실패하면 false 를 반환하는 것에 상당하며, 실패한 것은 ��지만 구체적인 이유는 모를 수 있습니다. Either 는 콜백 함수의 첫 번째 파라미터가 오류 정보를 운반하는 것에 상당하며, 비어있지 않으면 실패이며 구체적인 이유는 해당 파라미터의 값입니다
타입 별명
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
-- 等价于, 하나의 파라미터 유지
type IntAssocList v = Int v
파라미터가 충분하면 구체적 타입이고, 그렇지 않으면 파라미터가 있는 타입입니다
아직 댓글이 없습니다