Elm入门实践(二)——类型篇

记得Facebook曾经在一次社区活动上说过,随着他们愈来愈多地使用Javascript,很快就面临了曾经在PHP上遇到的问题:这东西究竟是啥?javascript

动态语言就像把双刃剑,你能够爱死它的灵活性,也可能由于一个小的疏忽而损失惨重。Elm选择了静态强类型,这一般也是多数函数式语言的选择,没有了OO语言中的概念,强大的类型系统负责解决一切“这是什么?”的问题html

类型注解

也能够叫作类型签名,Elm 使用冒号:来注明类型,在Hello world的基础上,让咱们分别定义一个变量和函数,而且注明类型java

import Html exposing (..)
import Html.Attributes exposing (..)

elm : String
elm = "elm"

sayHello : String -> String
sayHello name = 
  "Hello, " ++ name

main =
  div [class "hello"] 
    [ span [] [text (sayHello elm)]
    ]

尝试将elm的值"elm"改成数字,看看会发生什么?编程

Detected errors in 1 module.


-- TYPE MISMATCH ---------------------------------------------------------------

The type annotation for `elm` does not match its definition.

5| elm : String
         ^^^^^^
The type annotation is saying:

    String

But I am inferring that the definition has this type:

    number

编译器发现了错误,而且可以定位到具体的行数。redux

若是不声明类型呢?若是注释掉类型注解数组

import Html exposing (..)
import Html.Attributes exposing (..)

--elm : String
elm = 6

--sayHello : String -> String
sayHello name = 
  "Hello, " ++ name

main =
  div [class "hello"] 
    [ span [] [text (sayHello elm)]
    ]

从新编译,仍是会报错,只是错误信息变了,此次是第14行:安全

Detected errors in 1 module.


-- TYPE MISMATCH ---------------------------------------------------------------

The argument to function `sayHello` is causing a mismatch.

14|                      sayHello elm)
                                  ^^^
Function `sayHello` is expecting the argument to be:

    String

But it is:

    number

即便没有显式的类型注解,Elm的类型推导系统也会发挥做用,此处经过类型推导认为sayHello函数的参数应该是字符串,可是传入了数字,所以报错。数据结构

对比两次不一样的错误提示能够看出,类型注解能让编译器更准确地发现和定位错误。随着学习的深刻你会慢慢喜欢上类型系统带来的安全感:若是编译失败,明确的提示能帮助你快速定位问题。而只要编译经过,程序就必定能运行。架构

基本类型和List

基本类型

基本类型和多数语言是相似的,无非就是String, Char, Bool Int, Float,能够参考官网的literals。须要注意在Elm中,String必须用双引号,单引号是用来表示Char的,字符串单引号党须要适应一下。编辑器

List

严格来讲List并非类型,它的类型是List a,其中的a被称做类型变量,这是由于List做为容器,它能够装String,Int,或者什么都不装,所以类型必须是动态的:

> [ "Alice", "Bob" ]
[ "Alice", "Bob" ] : List String

> [ 1.0, 8.6, 42.1 ]
[ 1.0, 8.6, 42.1 ] : List Float

> []
[] : List a

关于类型变量后面会继续讨论,在这里咱们只须要记住一点:List不是类型,相似List String这样的才是

因为参数只有一个,Elm的List只能容纳单一类型的元素,和Javascript来者不拒的List不一样,下面这样的是会被编译器发现并报错的:

list = [1, "a"]

类型别名

类型别名用于组合或复用已知的类型,好比

type alias Name = String
type alias Age = Int
  
type alias User = {name: Name, age: Age}

user : User
user =
  { name = "Zhang zhe", age = 89 }
  
setUserName : String -> User -> User
setUserName name user = {user | name = name}

它不只可让基本类型具有业务语义,还能够为复杂的数据结构组合出合适的、语义化的类型。没有别名的话,setUserName的类型签名就得写成下面这样……一坨:

setUserName : String -> {name: String, age: Int} -> {name: String, age: Int}

Union Types

Union type 是Elm类型系统中最重要的部分之一,它用来表示一组可能的值,每一个值叫作一个Tag,以下:

type Bool = True | False

type User = Anonymos | Authed

其中TrueFalseAnonymosAuthed 都是Tag名(注意Tag不是Type)。看起来很像枚举?不仅这样,Union type强大的地方在于:Tag能够携带一组已知类型。上面的代码咱们虽然能区分两类用户,但并不能获取认证用户的名称,这时候就能够用已知类型结合Tag表达:

type User = Anonymos | Authed String

当咱们建立Union type的时候,实际上为每一个Tag都建立了相应的值构造器

> type User = Anonymous | Authed String

> Anonymous
Anonymous : User

> Authed
Authed : String -> User

不带其它信息的Anonymous能够直接做为使用(想一想TrueFalse),而带有已知类型的Authed其实是一个函数,它接受String,返回User类型:

users : List User
users = [
  Anonymous, 
  Authed "Kpax"]

在Haskell中没有Tag的叫法,类似的东西就叫值构造器(value constructor),直接的代表了它的用途:构建属于该类型的值

Tag还能够被解构:

getAuthedUserName : User -> String
getAuthedUserName user =
  case user of 
    Anonymous ->
      ""
    Authed name ->
      name

这个函数返回Authed用户的名称,若是是Anonymous用户则返回空字符串。

完整的可在在线编辑器中执行的代码以下:

import Html exposing (..)
import List

type User = Anonymous | Authed String

users : List User
users = [
  Anonymous, 
  Authed "Kpax",
  Authed "qin"]

getAuthedUserName : User -> String
getAuthedUserName user =
  case user of 
    Anonymous ->
      ""
    Authed name ->
      name      

main =
  div [] (List.map (text << getAuthedUserName) users)

text << getAuthedUserName 使用了Elm中的<<操做符实现两个函数的compose,相似于lodash中的_.flowRight函数

Type variables

上面已经提到了List a类型,其中a即类型变量,表示一个当前还不肯定的类型,相似于面向对象编程中泛型的概念

map函数的类型签名也使用了类型变量:

map : (a -> b) -> a -> b

这使得咱们调用map函数map userToString user时,只要保证user是User类型便可,map函数并不关心具体的类型。

那么如何定义一个List a类型呢?代码以下

type List a = Empty | Node a (List a)

前面说到Tag能够携带已知类型,那么是否能够携带正在定义的这个类型呢?答案是确定的!这就是类型的递归List a就是这样一个带有类型参数的递归类型,平时咱们写的数组,能够理解为以下代码的语法糖

-- []
Empty

-- [1]
Node 1 Empty

-- [1, 2, 3]
Node 1 (Node 2 (Node 3 Empty))

一样的思路,咱们彻底能够本身实现二叉树等数据结构,有兴趣的朋友不妨试试,官方文档有相关章节可供参考

Counter with type

上一章[基础篇]()咱们讲了Counter的实现,代码以下:

import Html exposing (..)
import Html.Events exposing (onClick)
import Html.App as App

type Msg = Increment | Decrement

update msg model = 
  case msg of 
    Increment -> 
      model + 1
    Decrement ->
      model - 1

view model =
  div []
    [ button [onClick Decrement] [text "-"]
    , text (toString model)
    , button [onClick Increment] [text "+"]
  ]

initModel = 3

main = App.beginnerProgram {model = initModel, view = view, update = update}

让咱们用刚刚学习的知识给以上代码添加类型和类型注解

首先,咱们有initModel这个数据,它的类型是Int,不具有任何业务语义,让咱们定义一个类型别名Model来表示Counter的数据

type alias Model = Int

天然initModel的类型应该为Model

initModel : Model
initModel = 3

update函数的类型签名比较简单,它接受消息Msg和当前数据Model,返回新的数据Model:

update : Msg -> Model -> Model

view函数接受Model类型的数据,返回什么呢?若是查阅div函数的文档,你会发现返回的是一个带有类型变量的类型Html msg。其实很好理解,由于渲染界面的函数不只要输出Html,当事件发生时还要输出消息,输出消息的类型,就是应该赋给变量msg的类型,在Counter中消息的类型是Msg,所以:

view : Model -> Html Msg

完整代码:

import Html exposing (..)
import Html.Events exposing (onClick)
import Html.App as App

type alias Model = Int

type Msg = Increment | Decrement

update : Msg -> Model -> Model
update msg model = 
  case msg of 
    Increment -> 
      model + 1
    Decrement ->
      model - 1

view : Model -> Html Msg
view model =
  div []
    [ button [onClick Decrement] [text "-"]
    , text (toString model)
    , button [onClick Increment] [text "+"]
  ]

initModel : Model
initModel = 3

main = App.beginnerProgram {model = initModel, view = view, update = update}

总结

类型的学习可能有些枯燥,可是很是重要。若是你了解redux,你会发现Union type简直天生就是作action的料,比起redux在javascript中使用的字符串既简洁又达意,甚至还能够嵌套组合,谈笑风生!高到不知道哪里去了!

下一章咱们将把在线编辑器放到一边,把Counter迁移到本地运行,而后实现一个CounterList,在CounterList中,你会看到Elm是如何复用组件,以及为何Elm被称为理想的分形架构。

各类架构对比,能够参考Cycle.js做者Andre Staltz的文章

相关文章
相关标签/搜索