Oosten Studio

这世界没有一件事情是虚空而生的。站在光里,背后就会有阴影,这深夜一片寂静,是因为你还没有听见声音。

Haskell

标签

《我的第一个面向需求的Haskell程序》续

前言

上一篇《我的第一个面向需求的Haskell程序》文章中的Haskell程序还存在一个问题: 程序只打印出了文件中有没有重复的元素但是并没有告知是哪一个元素重复了,重复了几次也没有打印出来。 所以我继续优化下上篇文章中的Haskell程序,现在这段程序变成了下面这样

代码

module Main where

import Data.List.Split
import Data.List
import System.IO
import System.Environment

main = do
    args <- getArgs
    check args

check::[String] -> IO ()
check [filename] = do
    contents <- readFile filename
    mapM_ printRepeat $ fmap (\(x:xs) -> (x, 1 + length xs)) $ group $ splitOn "\r\n" contents
    putStrLn "check done"

check x = do
    putStrLn "请输入文件名"

printRepeat::(String, Int) -> IO()
printRepeat (word, num)
    | num > 1 = putStrLn $ word ++ " repeated " ++ (show num) ++ " times."
    | otherwise = return ()

使用

$ cabal build
$ ./dist-newstyle/build/x86_64-osx/ghc-8.8.4/repeat-0.1.0.0/x/repeat/build/repeat/repeat test.txt
joM2qWfjOJc repeated 2 times.
check done

解释

首先我们使用split包提供的splitOn 函数按照换行符将文件内容切分为[String],现在我们有了:

["abc", "abc", "def", "ghi", "def"]

然后使用group函数聚合下这个List,得到:

[["abc", "abc", "abc"], ["def", "def"], ["ghi"]]

再通过fmap (\(x:xs) -> (x, 1 + length xs))即map一个lambda表达式到这个List上,将这个List中的每个元素转为元组,得到:

[("abc", 3), ("def", 2), ("ghi", 1)]

至此我们实际做了一个WordCount程序…

接下来调用printRepeat函数打印出来结果就OK了

阅读全文 »

对Haskell惰性求值的理解

全文均为伪代码,没有验证,只可意会

doIf::Bool -> a -> Maybe a
doIf cond action = if cond then (Maybe action) else Nothing

我们声明了一个doIf函数,它接收两个参数:cond与action,它干一件事:如果cond == true 就调用action,并包在Maybe中返回。 如果是strict的语言的话,在调用doIf函数的之时action就会被执行了,而Haskell默认是non-strict的,所以在调用doIf时,action无需求值,只有在cond为true时才需要对action求值,如果cond为false的话action根本不会被执行,这就是non-strict与strict的区别。

阅读全文 »

我的第一个面向需求的Haskell程序

背景

上周五(20年8月28日)的时候,公司测试同学需要测试我的一个提测需求,其中有个测试用例是需要检查下下后台导出的兑换口令列表文件中是否有重复的口令。

由于导出的口令有数百万之多,肯定是不能用眼去看了,原本是打算用excel来检查的,但是我一想:ei(二声)~,最近不是正好在搞Haskell吗?正好拿来练练手,用Haskell写个检测程序。

Why is Haskell

因为这个程序写出来是要交给测试同学使用的,如果用java或者php这种解释型语言来写,还需要测试同学先去安装个java/php的解释器才行,显然是有点扯的,所以用编译型语言写完后直接build出一个可执行文件才比较方便。

当然可以将java/php的程序打包成一个可执行文件,但是又要花费我一些不必要的时间了。

编译型语言中我常用的有golang和Haskell。不可否认Go面对这个需求写起来可能更快,但是我其实还是想用Haskell练练手。

那? 开始吧!

首先,使用cabal创建一个项目

$ mkdir repeat && cd repeat
$ cabal init

导出的口令文件是以\r\n换行的,haskell的lines函数无法切分,所以需要通过cabal引入一个包:split,我的repeat.cabal文件就变成了下面这样了:

cabal-version:       >=1.10
-- Initial package description 'repeat.cabal' generated by 'cabal init'.
-- For further documentation, see http://haskell.org/cabal/users-guide/

name:                repeat
version:             0.1.0.0
-- synopsis:
-- description:
-- bug-reports:
-- license:
license-file:        LICENSE
author:              wangdongdong
maintainer:          wangdongdong@smzdm.com
-- copyright:
-- category:
build-type:          Simple
extra-source-files:  CHANGELOG.md

executable repeat
  main-is:             Main.hs
  -- other-modules:
  -- other-extensions:
  build-depends:       base >=4.13 && <4.14, split
  -- hs-source-dirs:
  default-language:    Haskell2010

编辑Main.hs

module Main where

import Data.List.Split
import Data.List
import System.IO
import System.Environment

main = do
    args <- getArgs
    check args

-- 通过模式匹配获取命令行参数中的文件名
check::[String] -> IO ()
check [filename] = do
    contents <- readFile filename
    -- 暴力通过去重后的list length对比来判重,不可取
    if (length $ mylines contents) /= (length $ nub $ mylines contents)
        then putStrLn "有重复元素" 
        else putStrLn "没有重复元素"

check x = putStrLn "请输入文件名"

-- 通过split库的splitOn函数以\r\n为切割符将文件内容切分为list
mylines contents = splitOn "\r\n" contents

最后编译为可执行文件

$ cabal build

编译结果在dist-newstype文件夹之中

交付使用

$ ./repeat keywords.txt

能够满足需求!

后续优化请看

《我的第一个面向需求的Haskell程序》续

阅读全文 »

haskell 中的newtype

haskell中一般使用data关键字来自定义type,像这样:

data BookInfo = Book Int String [String] deriving (Show)

但有些情况下要使用newtype来定义, 举个例子,对于数字来说,它有两种选择可以表现为一个monoid,一个是 * 作为二元函数,1 作为identity, 另外一种是 + 作为二元函数,0 作为identity。那么问题来了怎么把这两种选择都实现 (这里所说的实现是指把一个数字实现为Monoid这个typeclass的instance) 呢?

Data.Monoid 这个模块导出了两个类型:ProductSum 。Product的定义如下:

Prelude Data.Monoid> :i Product
newtype Product a = Product {getProduct :: a}

Sum的定义如下:

Prelude Data.Monoid> :i Sum
newtype Sum a = Sum {getSum :: a}

Product的Monoid的instance实现:

instance Num a => Monoid (Product a) where  
    mempty = Product 1  
    Product x `mappend` Product y = Product (x * y)

很显然它将第一种选择即乘法实现了。它代表 Product a 对于所有属于 Numa 是一个 Monoid

为什么要用newtype呢?

因为newtype比较快。 如果用data的话在执行的时候会有包起来和解开来的成本,但使用newtype的话,Haskell会知道你只是要将一个type包成一个新的type,你想要内部运作完全一样只是要一个新type而已。有了这个概念,Haskell可以将包裹和解开的成本省掉。

为什么不能所有地方都用newtype呢,是因为当使用newtype来制作一个新type的时候,只能有一个值构造器,而且这个值构造器只能有一个字段。

阅读全文 »

一些范畴论上的概念

为了能真正理解Haskell中的Functor、Applicative、Monad、Monoid,以及它们到底有什么用,个人觉得还是有必要 了解 一些范畴论里面的概念的

函数 Function

函数表示特定类型之间的 态射

自函数 EndoFunction

自函数就是把类型映射到自身类型

identity :: Number -> Number

identity函数就是一个自函数的例子,它接收什么就返回什么

函子 Functor

函子与函数不同,函数描述的是类型之间的映射,而函子描述的是 范畴(category) 之间的映射

范畴

范畴是一组类型及其关系 态射 的集合。包括特定类型及其态射,比如: Int、 String、 Int -> String ;高阶类型及其态射,比如 List[Int]、 List[String]、 List[Int] -> List[String]

函子如何映射两个范畴

image.png

图中,范畴C1和范畴c2之间有映射关系,C1中Int映射到C2List[Int],C1中String映射到C2List[String],C1中的关系态射Int -> String 也映射到 C2中的关系List[Int] -> List[String]态射上。

也就是说,一个范畴内部的所有元素可以映射为另一个范畴的元素,且元素间的关系也可以映射为另一范畴中的元素间的关系,则设为这两个范畴之间存在映射。所谓函子就是表示两个范畴之间的映射。

Haskell中,Functor是可以被map over的东西,List就是一个典型的instance。构造List[Int] 就是把Int提升到List[Int],记作:Int -> List[Int] . 这表达了一个范畴的元素可以被映射为另一个范畴的元素

我们看下Haskell中map函数的定义:

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

把我们上面的Int String的例子代入,配合柯里化的概念可以得出:

map :: (Int -> String) -> (List[Int] -> List[String])

map的定义清晰的告诉我们: Int -> String 这个关系可以被映射为 List[Int] -> List[String] 这种关系。这就表达了元素间的关系可以映射为另外一个范畴元素间的关系

所以List就是一个Functor

自函子

自函数是把类型映射到自身类型,那么自函子就是把范畴映射到自身范畴。

image.png

上图就是一个将范畴映射到自身的自函子。从函子的定义出发,我们考察这个自函子,始终有List[Int] -> List[String]List[Int] -> List[String] -> List[Int] -> List[String] 这两种映射。我们表述为:

类型List[Int] 映射到自己
态射f :: List[Int] -> List[String] 映射到自己

我们记作:

F(List[Int]) = List[Int]
F(f) = f
其中F是Functor

幺半群

先解释下群的概念:G为非空集合,如果在G上定义的二元运算*,满足:

(1) 封闭性:(Closure):对于任意a,b∈G,有a*b∈G
(2) 结合律(Associativity):对于任意a,b,c∈G,有(a*b)*c=a*(b*c)
(3) 幺元 (Identity):存在幺元e,使得对于任意a∈G,e*a=a*e=a
(4) 逆元:对于任意a∈G,存在逆元a^-1,使得a^-1*a=a*a^-1=e

则称(G, *) 为群,简称G为群。

如果仅满足封闭性和结合律,则该G是一个 半群(Semigroup) ; 如果满足封闭性和结合律并且存在幺元,则该G是一个 幺半群(Monoid)

接下来看下在自函子的范畴上,怎样结合幺半群的定义得出Monad

假设我们有个cube函数,它计算一个数的三次方:

cube :: Number -> Number

现在我们想在其返回值上添加一些调试信息,返回一个元组,第二个元素代表调试信息,函数签名为:

f :: Number -> (Number, String)

可以看到参数与返回值不一致。我们再看下幺半群规定的结合律。对于函数而言,结合律就是将函数以各种结合方式嵌套起来调用。我们将Haskell中的 . 函数看做这里的二元运算。

(.) :: (b -> c) -> (a -> b) -> a -> c

f . f

从函数签名可以看出右边f返回的是元组(Number, String),而左侧的f接收的是Number。所以无法组合,他们彼此不兼容。

有什么办法能消除这种不兼容?结合前面所述,cube是一个自函数,元组(Number,String)在Hask范畴是一个自函子 (这个说法看起来并不准确,(?, String)才应该是一个自函子 ) , 理由如下:

F Number = (Number, String)
F Number -> Number = (Number,String) -> (Number,String)

如果输入和输出都是元组,结果会怎样呢?

fn :: (Number,String) -> (Number,String)
fn . fn

这样是可行的,在验证满足结合律之前,我们引入一个liftM函数来辅助将f提升成fn

liftM :: (Double -> (Double, String)) -> (Double,String) -> (Double, String)
liftM f (x,y) = case r of (n,s) -> (n, y ++ s)
    where r = f x 

没有验证,就当伪代码看吧

我们来实现元组自函子范畴上的结合律:

cube :: Number -> (Number, String)
cube x = (x * x * x, "cube was called.")

sine :: Number -> (Number, String)
sine x = (Math.sin x, "sine was called.")

f = ((liftM sine) . (liftM cube)) . (liftM cube)
f (3, "")
输出:(0.956, 'cube was called.cube was called.sine was called.')

f1 = (liftM sine) . ((liftM cube) . (liftM cube))
输出:(0.956, 'cube was called.cube was called.sine was called.')

这里f和f1代表的结合顺序产生了相同的结果,说明元组自函子范畴满足结合律。

那如何找到这样一个e,使得 a * e = e * a = a ,此处的 * 就是 .

unit :: Number -> (Number, String)
unit x = (x, "")

f = (liftM sine) . (liftM cube)

f . (liftM unit) = (liftM unit) . f = f

这里的 liftM unit 就是 e 了。

unit 个人理解应该就是类型构造器

阅读全文 »

Haskell lambda 与 $ 与 函数组合

lambda

lambda就是匿名函数,有些时候我们会需要一个函数而这个函数可能只用到一次,并没有重用的场景,我们就可以搞一个 临时 的匿名函数来满足我们的计算。

(\xs -> length xs > 10)

lambda首先是一个\,后面是用空格分隔的参数,->后边就是函数体。通常会用括号括起来。

$

$函数,也叫作函数调用符,它的定义如下

($) :: (a -> b) -> a -> b  
f $ x = f x  

普通的函数调用符有最高的优先级,而 $ 的优先级则最低。用空格的函数调用符是左结合的,如 f a b c 与 ((f a) b) c 等价,而 $ 则是右结合的

$是优先级最低的中缀右结合函数,从签名来看,只是个函数调用符,相当于在右边加括号

tip: $是个中缀函数,要求左边是函数,右边是其参数

> max 5 3 * 2 + 1
11
> max 5 $ 3 * 2 + 1
7
# 函数组合
函数组合用```.```函数来实现,```.```函数的定义为:

(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)

函数组合的用处之一就是生成新函数,并传递给其他函数。  
假设我们有一个数字组成的list,我们要把它其中每个元素转成负数,在使用函数组合之前我们可能会这样实现:

Prelude> map (\x -> negate (abs x)) [1,2,-3,4,5,-6] [-1,-2,-3,-4,-5,-6]

> tip: 先用abs函数取绝对值,再用negate函数取反

用函数组合的话就可以这样实现:

Prelude> map (negate . abs) [1,2,-3,4,5,-6] [-1,-2,-3,-4,-5,-6]

函数组合的另一用途就是定义 point free style (也称作 pointless style) 的函数。以下面的函数为例:

sum’ :: (Num a) => [a] -> a
sum’ xs = foldl (+) 0 xs

等号的两端都有个 xs。由于有柯里化 (Currying),我们可以省掉两端的 xs。foldl (+) 0 回传的就是一个取一 List 作参数的函数,我们把它修改为 sum' = foldl (+) 0,这就是 point free style。下面这个函数改成point free style就是:

fn x = ceiling (negate (tan (cos (max 50 x))))

fn = ceiling . negate . tan . cos . max 50 “`

阅读全文 »

Haskell 函数语法

模式匹配

Prelude> let { lucky' :: Integral a => a -> String; lucky' 7 = "seven"; lucky' x = "other" }
Prelude> lucky' 7
"seven"
Prelude> lucky' 10
"other"
Prelude> lucky' "a"

<interactive>:39:1: error:
    • No instance for (Integral [Char]) arising from a use of ‘lucky'’
    • In the expression: lucky' "a"
      In an equation for ‘it’: it = lucky' "a"

tip: 在8.8.1版本ghci中如果按照书中的写法是会报没有匹配项的错误的,按照let { … } 写法则没有问题

在调用lucky时,模式会从上到下进行检查,一旦有匹配则对应的函数体便被应用。

如果我们制定的匹配模式不全时,传入一个没有被任何模式匹配到的参数时就会报错。

对Tuple同样可以使用模式匹配:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)  

first :: (a, b, c) -> a  
first (x, _, _) = x 

List Comprehension 也可以用模式匹配:

Prelude> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]  
Prelude> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]

对List也可以用模式匹配:

Prelude> let {sumList :: Num a => [a] -> a; sumList [] = 0; sumList (x:xs) = x + sumList xs }
Prelude> sumList [1,2,3]
6

Guards

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                 = "You're a whale, congratulations!"    

guard由跟在函数名及参数后边的竖线标志,通常竖线都是靠右一个缩进排成一列。一个guard就是一个布尔表达式,如果是True,就使用对应的函数体。最后的一个guard往往是otherwise,它的定义就是简单一个otherwise = True。

通过guard实现自己的compare函数

-- myCompare.hs
myCompare :: Ord a => a -> a -> Ordering
a `myCompare` b 
    | a > b = GT
    | a < b = LT
    | otherwise = EQ
Prelude> :l myCompare.hs
[1 of 1] Compiling Main             ( myCompare.hs, interpreted )
Ok, one module loaded.
*Main> 1 myCompare 2

<interactive>:2:1: error:
    • Non type-variable argument
        in the constraint: Num ((a -> a -> Ordering) -> t1 -> t2)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall a t1 t2.
              (Ord a, Num t1, Num ((a -> a -> Ordering) -> t1 -> t2)) =>
              t2
*Main> 1 `myCompare` 2
LT
*Main> 2 `myCompare` 1
GT
*Main> 1 `myCompare` 1
EQ

where 关键字

上面例子中的bmiTell函数weight / height ^ 2重复计算了3次,可以利用where修改:

-- bmiTell.hs

bmiTell :: RealFloat a => a -> a -> String
bmiTell weight height 
    | bmi <= 18.5 = "case 1"
    | bmi <= 25.0 = "case 2"
    | bmi <= 30.0 = "case 3"
    | otherwise = "otherwise"
    where bmi = weight / height ^ 2
*Main> :l bmiTell.hs
[1 of 1] Compiling Main             ( bmiTell.hs, interpreted )
Ok, one module loaded.
*Main> bmiTell 130 180
"case 1"

还可以继续修改:

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | bmi <= skinny = "You're underweight, you emo, you!"  
    | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | bmi <= fat    = "You're fat! Lose some weight, fatty!"  
    | otherwise     = "You're a whale, congratulations!"  
    where bmi = weight / height ^ 2  
          skinny = 18.5  
          normal = 25.0  
          fat = 30.0 

函数在where绑定中定义的名字只对当前函数可见。 where绑定也可以使用模式匹配

...
where bmi = weihgt / height ^ 2
    (skinny, normal, fat) = (18.5, 25.0, 30.0) 

where绑定可以定义名字也可以定义函数:

calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi w h | (w, h) <- xs] 
    where bmi weight height = weight / height ^ 2  

let关键字

let绑定是个表达式,允许在任何地方定义局部变量,而对不同的guard不可见。let也可以使用模式匹配

cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h = 
    let sideArea = 2 * pi * r * h  
        topArea = pi * r ^2  
    in  sideArea + 2 * topArea  

let的格式为let [binging] in [expression]。let中绑定的名字仅在in中可见,let中的名字必须对齐在一列。

let是个表达式,而where是个语法结构。因为let是个表达式,所以let可以随处安放:

Prelude> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]

上面的例子中let定义了一个函数。

若要在一行中绑定多个名字,可以用分号将他们分开:

Prelude> (let a = 100; b = 200; c = 300 in a*b*c, let foo = "Hey"; bar = "there" in foo ++ bar)
(6000000,"Heythere")

tip: 最后那个绑定后面的分号不是必须的,可以加上可以去掉

可以用let改写上面的calcBmis函数:

calcBmis xs = [bmi w h | (w, h) <- xs, let bmi = w / h ^ 2]

List Comprehension 中 let 绑定的样子和限制条件差不多,只不过它做的不是过滤,而是绑定名字。let 中绑定的名字在输出函数及限制条件中都可见。这一来我们就可以让我们的函数只返回胖子的 bmi 值:

calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0] 

在 (w, h) <- xs 这里无法使用 bmi 这名字,因为它在 let 绑定的前面。

在 List Comprehension 中我们忽略了 let 绑定的 in 部分,因为名字的可见性已经预先定义好了。不过,把一个 let…in 放到限制条件中也是可以的,这样名字只对这个限制条件可见。在 ghci 中 in 部分也可以省略,名字的定义就在整个交互中可见。

Prelude> let a = 1
Prelude> a
1

Case 表达式

head' :: [a] -> a  
head' xs = case xs of [] -> error "No head for empty lists!"  
                      (x:_) -> x  

case的语法:

case expression of pattern -> result  
                   pattern -> result  
                   pattern -> result  
                   ...  

expression匹配符合的模式,如果符合则执行。实际上上面的模式匹配是case的语法糖而已。

函数参数的模式匹配只能用在定义函数时使用,而case可以用在任何地方:

describeList :: [a] -> String  
describeList xs = "The list is " ++ case xs of [] -> "empty."  
                                               [x] -> "a singleton list."   
                                               xs -> "a longer list." 
阅读全文 »

Haskell Type与Typeclass

Type

ghci中可以用:t检测表达式的类型

Prelude> :t "a"
"a" :: [Char]

函数也有类型,编写函数时给一个明确的类型声明是一个好习惯

removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]

我们可以这样解读这个函数的类型:removeNonUppercase这个函数接收一个Char List类型的参数返回一个Char List类型的返回值

addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

参数之间由->分隔,这样解读这个函数的类型:addThree这个函数接收3个Int类型的参数返回一个Int类型的返回值。
> tip: 按照其他语言中的习惯,Int,Int,Int -> Int好像看起来更为恰当一些,但实际haskell中->只有一个作用:它标识一个函数接收一个参数并返回一个值,其中->符号左边是参数的类型,右边是返回值的类型。haskell中所有函数都是只接收一个参数的,所有函数都是currying的。

常见类型

  • Int 整数,与平台位数相关
  • Integer 无限大整数
  • Float 单精度浮点数
  • Double 双精度浮点数
  • Bool
  • Char

Tuple的类型取决于它的长度与其中项的类型,空Tuple也是一个类型,它只有一个值()

Type variables

以head函数为例

Prelude> :t head
head :: [a] -> a

可以看到这里有个a,而a明显不是一个具体的类型,类型首字母必须是大写的,那它是什么呢,它实际上是一个类型变量,a可以是任意类型。 > tip: 与其他语言中的泛型generic很像

使用到类型变量的函数被称为“多态函数”。可以这样解读head函数的类型:head函数接收一个a类型的List参数(即任意类型的参数)返回一个a类型的返回值(参数与返回值的类型必须是一样的,都是a类型)
fst函数的类型:

Prelude> :t fst
fst :: (a, b) -> a

可以看到fst取一个包含两个型别的 Tuple 作参数,并以第一个项的型别作为回传值。这便是 fst 可以处理一个含有两种型别项的 pair 的原因。注意,a 和 b 是不同的型别变量,但它们不一定非得是不同的型别,它只是标明了首项的型别与回传值的型别相同。

Typeclass

如果一个类型属于某个typeclass,那它必定实现了Typeclass所描述的行为。

tip: 跟OOP中的接口很像

==函数的类型声明为例:

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

这里的Eq就是typeclass, 这里意思是说a这个type必须是Eq的一个实现(相当于OOP中的a implement Eq)
=>符号左边的部分叫做类型约束

Eq这个Typeclass提供了判断相等性的接口,凡是可比较相等性的类型必定属于Eq class

elem函数的类型为:(Eq a)=>a->[a]->Bool这是因为elem函数在判断元素是否存在于list中时使用到了==的原因。

Show的成员为可用字符串表示的类型,操作Show Typeclass最常用的函数表示show。它可以取任一Show的成员类型并将其转为字符串

Prelude> show [1,2,3]
"[1,2,3]"
Prelude> show True
"True"

ReadShow相反,read函数可以将字符串转为Read的某成员类型

Prelude> read "5" - 2 
3
Prelude> read "True" || False
True

但是执行下面的代码,就会提示错误:

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

这是因为haskell无法推导出我们想要的是一个什么类型的值,read函数的类型声明:

Prelude> :t read
read :: Read a => String -> a

它的回传值属于Read Typeclass,但是如果我们用不到这个值,它就无法推导出这个表达式的类型。所以我们需要在表达式后跟::的类型注释,以明确其类型:

Prelude> read "5" :: Int
5
阅读全文 »

Haskell 基础

第一个函数

创建doubleMe.hs文件,编写如下代码:

doubleMe x = x + x

保存,打开ghci,输入

Prelude> :l doubleMe.hs

这样我们就加载了我们的doubleMe函数,然后就可以调用这个函数:

Prelude> doubleMe 10
20

tip: 如果修改doubleMe.hs文件需要重新导入的话可以执行:reload doubleMe.hs或者:r doubleMe.hs重新导入

if语句

Haskell中的if语句与其他语言不同,else是不可以省略的

doubleSmallNum x = if x > 10 then x else x * 2

Haskell 中的 if 语句的另一个特点就是它其实是个表达式,表达式就是返回一个值的一段代码:5 是个表达式,它返回 5;4+8 是个表达式;x+y 也是个表达式,它返回 x+y 的结果。正由于 else 是强制的,if 语句一定会返回某个值,所以说 if 语句也是个表达式。

List

列表由方括号以及被逗号间隔的元素组成:

Prelude> [1,2,3]
[1,2,3]

空列表:[],列表中所有元素必须是同一类型。

列表操作符

用 ++ 操作符连接两个list

Prelude> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]

用 : 连接一个元素到list头,它读作“cons”即construct简称

Prelude> 1:[2,3]
[1,2,3]

但是[2,3]:1是不被允许的,因为:的第一个参数必须是单个元素,第二个参数必须是list

字符与字符串

Prelude> "this is string"
this is string

双引号表示字符串。单个字符用”表示

Prelude> 't'
t

字符串实际是字符列表,

Prelude> 't' : "his is string"
this is string
Prelude> "this is" ++ " string"
this is string

操作

从list中取值使用!!(相当于其他语言中的arr[index])

Prelude> let l = [1,2,3]
Prelude> l!!1
2

上面的例子就是从列表l中取下标为1的元素
list可以用来装list:

Prelude> let l = [[1,2,3], [1,2,3,4], [1,2,3,4,5]]

haskell不要求每个元素的长度一致,但要求类型必须一致

  • head函数取list第一个元素
  • tail函数取list除第一个元素之后的全部
  • last返回list最后一个元素
  • init返回一个除去list最后一个元素的全部
  • length返回list长度
  • null判断list是否为空,如果是空返回True,否则False
  • reverse 反转list
  • take 返回前几个元素
  • maximum 返回最大元素
  • minimun 返回最小元素
  • sum 返回所有元素之和,product返回积
  • elem 判断一个元素是否存在于list中,通常中缀调用 Prelude> tail [[1,2,3], [1,2,3,4], [1,2,3,4,5]] [[1,2,3,4], [1,2,3,4,5]] Prelude> init [[1,2,3], [1,2,3,4], [1,2,3,4,5]] [[1,2,3], [1,2,3,4]] Prelude> reverse [1,2,3] [3,2,1] Prelude> take 2 [1,2,3] [1,2] Prelude> 1 `elem` [1,2,3] True

Range

可以用列表符号来表示一系列元素,haskell会自动推导:

Prelude> [1..10]
[1,2,3,4,5,6,7,8,9,10]

Prelude> [1.0, 1.25, ..2.0]
[1.0,1.25,1.5,1.75,2.0]

Prelude> [1, 4, 15]
[1, 4, 7, 10, 13]

之所以没有输出15是因为15不属于我们定义的系列元素

List Comprehension

Prelude> [x*2 | x <- [1...10]]
[2,4,6,8,10,12,14,16,18,20]

可以给这个comprehension加个限制条件:

Prelude> [x*2 | x <- [1...10], x*2 > 12]
[14,16,18,20]

下面写一个函数,该函数使list中所有>10的奇数变为”BANG”,小于10的奇数变为BOOM:

bangBoom xs = [if x > 10 then "BANG" else "BOOM" | x <- xs, odd x]

tip: odd函数判读x是否是奇数,如果是则返回True

还可以从多个list中取元素:

[x*y | x <- [1,2,3], y <- [4,5,6]]
[4,5,6,8,10,12,12,15,18]

实现自己的length函数:

length' xs = sum [1 | _ <- xs]

_表示我们不会用到这个值
操作含有 List 的 List

Prelude> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
Prelude> [ [ x | x <- xs, even x ] | xs <- xxs]
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

Tuple

(1,2)      (True, "a", 1)

Tuple List:

[(1,2),(3,4),(5,6)]

但是[(1,2),(3,4,5),(5,6)]是会报错的,因为元素类型不一致
两个元素的Tuple可以称为序对(Pair) Tuple不能是单元素的,因为没有意义

操作函数

  • fst 返回序对的首项(只能操作序对,不能操作三元组等其他数量的Tuple)

  • snd 返回序对的尾项

    Prudule> fst (1,2,[1,2,3])
    1
    Prudule> snd (1,2,[1,2,3])
    [1,2,3]
    
  • zip 将两个list交叉配对生成一组Pair

    Prudule> zip [1 .. 5] ["one", "two", "three", "four", "five"]
    [(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]
    Prudule> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
    [(5,"im"),(3,"a"),(2,"turtle")]
    

若是两个不同长度的 List,较长的那个会在中间断开,去匹配较短的那个

阅读全文 »

ghci中的一些命令与case 【持续更新...】

ghic中模式匹配

按照rwh书中模式匹配一节中sumList的例子在ghci敲出这样的代码:

Prelude> sumList (x:xs) = x + sumList xs
Prelude> sumList [] = 0

调用这个函数时是会报一个错误的:

Prelude> sumList [1,2,3]
*** Exception: <interactive>:2:1-14: Non-exhaustive patterns in function sumList

而实际如何要在ghci中做一个模式匹配函数的话应该这样写:

Prelude> let { sumList' [] = 0; sumList' (x:xs) = x + sumList' xs }
Prelude> sumList' [1,2,3]
6

ghci中切换工作目录与查看当前工作目录

Prelude> :cd /tmp/
Prelude> :show paths
current working directory: 
  /tmp
module import search paths:
  .

使用:cd 命令切换到指定目录
使用:show paths目录查看当前工作目录

阅读全文 »
   第 1 页    下一页 »
© 2021 Oosten Studio