通过Spark SQL从Hive读取大约1M行的数据,一次性写入Kafka时,大概会丢失20%的数据。
producer
的设置只设置了acks=all
,使用的是异步的send()
。
Kafka Cluster有4个brokers。
首先,注意到producer
在程序结束时没有调用flush()
,加上flush()
之后,问题依旧。
从Kafka The Definitive Guide中了解到,设置retries
参数可以令producer自动重试retriable errors,设置retries=100
,问题依旧。
重试也不能解决这个问题,那么将异常打印出来试试,在send()
中传入一个失败时打印异常的Callback
,顺便记录下失败次数:
1 | producer.send(record, new Callback { |
再次运行,在Spark Executors的stderr中可以看见:
1 | 20/03/30 17:33:43 ERROR root: failed to send message: ProducerRecord(......) |
嗯?TimeoutException
?
这里说问题是在于send()
太快,KafkaProducer
有一个buffer(参数buffer.memory
)来存放即将发送的ProducerRecord
,当这个buffer满了的时候,后续的send()
就会阻塞(或抛出异常,根据配置而定),这个阻塞的上限时间由参数request.timeout.ms
确定,超出就会抛出TimeoutException
。
由于我们是一次性写入大量数据,可能producer处理不来,就会造成这种情况。
request.timeout.ms
默认是30000,也就是30s,尝试增大为300s后,丢失数据量降低至8%左右。
至此,问题的原因就找到了。
难道要继续增大超时时间吗?这里的本质原因是producer处理吞吐量不够。所以增加producer的数量也是一种办法。
这是一个Spark SQL程序,每个parition分配了一个Kafka producer。
通过Spark Web UI监控到,当执行这部分代码的时候,只有两个task,推测是由于DataFrame的partition数量过少。
根据这里的说法,HDFS blockSize决定了Spark SQL读取Hive的DataFrame的partition数量,Hadoop 2.x的默认blockSize是大小是128MB。
假设我们一条消息占用20bytes,一百万条也才20M,生成这部分Hive数据是顺序写入的,所以默认的parition数量只有一个。
那么,尝试一下手动增大partition数量:
1 | // before |
问题解决了,数据一条都没有丢。
KafkaProducer
的异步send()
,在buffer满了的时候,也会block;感谢@liuyuan大佬为我解惑,问啥啥都懂,太强了!
我常用的浏览器是Chrome,所以就用Tampermonkey来加载脚本。
原理也很简单,等页面加载完成后,我们可以用Tampermonkey再跑一段自定义的JavaScript,用来操作DOM树。
然后通过Chrome的审查元素,得知,需要去除的元素id分别为bili_report_live
和reportFirst2
。
根据我十分浅薄的前端知识,写下了非常naïve的两行:
1 | document.getElementById("bili_report_live").remove(); |
实验发现,在REPL(Console tab in dev tools)里输入这两行是可行的,但是放在Tampermonkey中,就不行了。
没关系,我们依然可以在REPL中看见报错信息:
1 | Uncaught TypeError: Cannot read property 'remove' of null |
NPE了,推测是脚本执行时机的问题,Tampermonkey执行脚本的时候,我们需要的节点还没有生成。
既然如此,我们尝试等待一会儿再执行。
1 | setTimeout(() => document.getElementById("bili_report_live").remove(), 500) |
尝试了下500ms的超时,在大多数情况下可以正确处理,但是如果网络波动了,就会失败。
除此之外,还有点令人不爽,你会看着这两块内容先出现,再消失。
看看还有没有别的办法。
CSS的id selector是个好选择,DOM树中有了就自动应用样式,没有的话也不影响。
1 | (document.head || document.documentElement) |
这个需求其实有个简单的思路:我希望能在DOM树变更的时候被通知到。
在MDN搜索了一下,果然有这个API:MutationObserver
这样写也就OK了:
1 | new MutationObserver(function(mutations) { |
v3和v4都是不错的方法,能够完美解决这个问题,我的前端知识又前进了一小步。
现在b站首页看起来就是这个样子了,很清爽,哈哈哈哈哈哈。
完整脚本如下:
1 | // ==UserScript== |
ForkJoinPool
是实现了work stealing的线程池,其中所有线程都是daemon thread。
1 | // Java 7+ |
1 | // Java 8+ |
1 | // Scala |
三个例子分别是:
CompletableFuture
组合异步计算:其中每个以Async结尾的方法,都再次将任务提交给了线程池,大概率会在另外一个线程中执行;.parallel()
并行处理Stream:.sum()
对并行Stream有优化,可以提升效率;.par
将Vector
变成并行的进行fold
:Monoid满足结合律,所以可以并行fold
。承载这些异步、并行计算的线程池,默认会使用一个JVM为我们生成的ForkJoinPool
,可以用ForkJoinPool.commonPool()
得到实例。
除此之外,Scala中的scala.concurrent.Future
一般会使用到scala.concurrent.ExecutionContext.Implicits.global
,而后者就是包装了这个common pool。
没错,ForkJoinPool
实现了ExecutorService
接口,是个线程池。
对于common pool,我们可以看见它的默认大小:
1 | // CPU: i7-7920HQ(4 Cores, 8 Threads @3.10 GHz): |
相比其他线程池,ForkJoinPool
有两个显著的特点:
ForkJoinPool
会先增大并发度(加线程),再处理):managedBlock(ForkJoinPool.ManagedBlocker blocker)
来替代submit
、execute
等Future
使用scala.concurrent.blocking
CompletableFuture
的join()
或者CountDownLatch
。上面描述的用法,主要在利用了work stealing以及common pool。
仔细浏览一下ForkJoinPool
的API,会发现有一个长得很像的类ForkJoinTask
。
1 | public final ForkJoinTask<V> fork() |
ForkJoinTask
有这么几个特点:
Future
使用。具体实践的例子可以参考这里Guide to the Fork/Join Framework in Java。
不过这种并发模型挺诡异的,可维护性也比较差。如果能够使用别的模型,还是尽量不要用这个。
]]>在FP的路上,不可避免地会碰到monad这个拦路虎。绕是绕不过去的,那就学咯。
“我看了几十篇关于monad的文章,还是没懂。” – 某不知名FP爱好者
这篇文章也不是silver bullet,我只希望读者在读过以后,对monad能有个大致的、模糊的印象,今后能够持续地从多个角度去审视这个概念,加深认识。
我们在说functor的时候,有一个不那么准确的定义:有map
的,就是functor。
当我们这样定义的时候,我们其实是在泛化map
,将它们的共性抽象出来,这个抽象就是functor。
同样地,flatMap
也出现在很多地方(比如List
,Option
,Future
,Either
等等等),我们自然也想把共性抽象出来,这个抽象,就是monad。所以可以类似地说:有flatMap
的,就是monad。
好吧,如果要正式一点,引用下scala with cats里的定义:monad是一种将计算按顺序排列起来的机制(a mechanism for sequencing computations)。
flatMap
是什么如果你熟悉flatMap
,可以跳过这一小节。
1 | case class Person(name: String, friends: List[String]) |
对于List[A]
来说,map
和flatMap
的签名,仅仅是传入的f
的返回类型不一样:
1 | def map[A, B](f: A => B): List[B] |
直观来讲,flatMap
将List
内的每一个A,转换成了一个List[B]
,然后将所有的List[B]
,连了起来。
我们知道,Option
可以用来表示结果是否存在。
考虑整数除法:
1 | // not pure, can throw exception |
现在我们不仅要做除法,还要让用户输入这两个值,我们需要一个parseInt
1 | def naiveParseInt(s: String): Int = s.toInt |
现在我们把他们串起来,自然地,我们也需要返回一个Option[Int]
来涵盖结果的成功和失败。
1 | def stringDiv(a: String, b: String): Option[Int] = |
stringDiv
做了这些事情:
parseInt(a)
返回一个None
或者Some(x)
;Some
,将Some
内的值x
取出来,传递给后面的函数;parseInt(b)
返回一个None
或者Some(y)
;Some
,将Some
内的值y
取出来,继续传给后面的函数;div(x, y)
,返回最终结果None
或者Some(x)
。手动使用flatMap
拼接比较繁琐,也不太容易看得清。Scala为我们提供了一个语法糖:for-comprehension。
下面的stringDiv2
与stringDiv是等价的,编译器会帮我们把for-comprehension编译成flatMap
(和map
)的调用链。
1 | def stringDiv2(a: String, b: String): Option[Int] = |
审视一下上面的例子,我们确确实实地将计算给串起来了。
flatMap
起了什么作用呢?
flatMap
将上游Option[Int]
中的值抽出来,执行运算,再塞进Option[Int]
中,传递给下游。
因为div
只能接受Int
,而传给div
的是Option[Int]
,所以我们不能用简单的函数组合来解决问题。
using compose | using flatMap |
---|---|
naiveParseInt: String => Int | parseInt: String => Option[Int] |
naiveDiv: (Int, Int) => Int | div: (Int, Int) => Option[Int] |
(a, b) => naiveDiv(naiveParseInt(a),naiveParseInt(b)) | see stringDiv2 |
Monad
的定义在上述的例子中,我们使用flatMap
将Option[Int]
串了起来。
如果我们只有Int
,flatMap
岂不是毫无作用?
没关系,我们还有unit
,可以把一个值A
变成F[A]
,可以视作将一个纯粹的值提升到了monad上下文中。
1 | trait Monad[F[_]] { |
我们说过,monad是特殊的functor。换言之,所有的monad都是functor,不是所有的functor都是monad。
因为我们可以使用unit
和flatMap
实现map
,有了map
,自然就是functor。
1 | def map[A, B](ma: F[A])(f: A => B): F[B] = ??? |
这个实现是显然且唯一的,答案在文章末尾。
这次我们通过一个简单的例子阐述了monad的作用,并写下了monad的一种形式化定义。
接下来,我们还会研究monad定律,研究monad的不同表现形式。
最后,我们会研究大量的monad实例,来加深理解。
unit
和flatMap
实现map
1 | def map[A, B](ma: F[A])(f: A => B): F[B] = |
先看一段代码:
1 | // js |
这些类型都有map
,而且看上去map
的作用好像都相同。
事实上,它们确实相同。
不那么准确地说,任何东西只要有map
,我们就可以将它视作functor。
要研究functor,我们需要转变一下(命令式)思路。
可以将functor想象成一个容器,容器里放了一些元素。
map
并不是对容器进行一次遍历(traverse),而是对容器内的元素做一个变换(transform)。
如果有多个map
被串起来了,则会按照先后顺序,进行变换。(这里顺序是很重要的,下面会细说)
我们可以将functor定义为一个trait:
1 | trait Functor[F[_]] { |
对于一个数据结构,我们只需要让其继承这个trait,并实现map
,就可以把它当作functor来用了。
比如:
1 | val listFunctor: Functor[List] = new Functor[List] { |
functor法则是不言自明的:
1 | map(s)(a => a) == s |
即,用identity函数映射一个结构s,结果仍然是s。
观察一下,我们可以发现functor能做的事仅有:
除此之外,都不可以。
比如这些事情,functor都是做不到的:
List
变成一个Set
,Some
变成一个None
;List
的第一个元素;Future
也有map
方法,也是一个functor 。
map
会将异步计算应用于Future
内的元素,多个map
会按照顺序先后执行。
1 | import scala.concurrent.{Await, Future} |
当我们使用Future
的时候,其实我们并不清楚Future
内部状态。
如果Future
已经完成了,那么我们的map
就会立刻被调用。
如果Future
尚未完成,我们的map
会被放到默认的ExecutionContext
里的队列中,等着,之后再执行。
对于Future
上的map
,我们并不知道map
何时会执行,我们只知道map
执行的顺序。
考虑这种情况,我们有一个f: T => A
和一个p: A => B
,想要得到一个g: T => B
。
你一定会说,这个简单,基本的function composition嘛。
这里我们需要换一个视角:
T => A
看作SomeFunc[A]
SomeFunc[A]
一个A => B
SomeFunc[B]
怎么样,我们是不是对SomeFunc
进行了一次map
。
看一段例子,这里我们用到了cats这个库,cats为Scala提供了很多函数式的抽象。
1 | import cats.instances.function._ |
当我们写下val f = f1.map(f2).map(f3).map(f4)
的时候,这一串map
就是将一系列的计算’串’在了一起,等到f
被调用的时候再执行(和Future
一样)。
PS: 这个我们其实叫它Reader Functor(详细内容可以看references中第一篇)。
functor是FP中非常非常基础的抽象,就像monoid一样,看上去对我们日常的编程并没有太大的作用(其实我们可以根据functor构建通用的zip
和unzip
)。不过没关系,之后我们还会研究一些functor的特例,monad和applicative functor,这些是十分实用且普遍的。
PS:你可以在范畴论中找到functor的原型,functor是范畴的态射(morphism of categories),会将一个范畴中的morphism和object映射到另一个范畴中。
考虑类型A
和C
,以及泛型构造器F<T>
:
A
的地方,总能使用C
,我们就可以说C
是A
的子类型(subtype)[1],记作C <: A
。C <: A
,则有F<C> <: F<A>
。C <: A
,则有F<A> <: F<C>
。A
与C
是什么关系,F<C>
与 F<A>
都没有关系。先举用例子来看看Collection的型变规则。
Duck
是Bird
的子类,Bird
是Animal
的子类,记作Duck <: Bird <: Animal
。
TypeScript中的Array
是协变的(covariant):
1 | interface Animal { name: String } |
函数传入参数同样遵循这个规则:
1 | function birdsFly(birds: Array<Bird>) { |
默认情况下,Java的List
是不变的(invariant):
1 | List<Bird> birds = new ArrayList<Duck>(); // Illegal |
当然,我们可以用bounded wildcards在使用时去除不变性:
1 | List<? extends Bird> coList = new ArrayList<Duck>(); // Legal; covariant |
Effective Java中提到的PECS(producer extends,consumer super),就是说,如果你接收一个Collection:
List
(Collection是consumer),那么你最好接受一个逆变的Collection。1 | void namesOfBirds(List<? extends Bird> birds) { |
容器类型的型变其实是比较符合直觉的,函数类型就很反直觉了。
1 | trait Function1[-T1, +R] |
这是Scala标准库定义的单参函数的trait(可以理解为interface),类型为T1 -> R
,其中T1
是逆变的,R
是协变的[2]。
But why???
回想一下,型变规则决定了我们能使用什么样的子类型,子类型决定了我们能在一个期望X的地方,安全地使用另一个类型Y(X的子类型)。
So,既然函数类型也是一种类型,我们当然也要知道函数类型的子类型咯,对吧?
那么对于函数类型Bird -> Bird
,根据trait定义,Animal -> Duck
就是它的子类型。
Weird, right?
假设我们有高阶函数f: (Bird -> Bird) -> String
(返回什么我们并不关心)。
将传入的函数记作g
,尝试在g
的参数上用子(超)类型进行替代:
1. 如果g: Duck -> Bird
,f(g)
会怎样?
f
已知g
接受Bird
,于是f
调用g
的时候,传给g
一个Bird
。
然而,g
只能处理Bird
的一个子类型Duck
,如果接收到一个Swan
,g
会崩溃。
所以参数类型不可以用子类型替代。
2. 如果g: Bird -> Animal
,f(g)
会怎样?
f
已知g
返回Bird
,于是f
会将g
的返回值当作Bird
来使用。
但是现在g
会返回一个不是Bird
的Animal
,比如Dog
,那么f
会崩溃。
所以返回类型不可以用超类型替代。
3. 如果g: Animal -> Duck
,f(g)
又如何呢?
f
已知g
接收Bird
,所以传给g
一个Bird
,g
可以正常处理Bird
并返回Duck
,没有问题。
f
已知g
返回Bird
,所以将g
返回的Duck
当作Bird
来用,所有Duck
都是Bird
,也没问题。
所以,在一个期望(Bird -> Bird)
的地方,我们总是可以使用(Animal -> Duck)
。
即(Animal -> Duck)
是(Bird -> Bird)
的子类型。
也就是说,参数类型可以用超类型替代,返回类型可以用子类型替代。
形式化一点,就是对于函数类型构造器->
,接收参数逆变且返回参数协变时,仍然可以保证类型安全。
或者说:对于函数p和q,当p的参数更general,返回值更specific时,p可以安全地替代q。
其实,Java8的Function
接口的型变规则也是这样的,compose
和andThen
都是接收了一个Function<? super V, ? extends T>
,恰恰正是参数逆变,返回值协变。
1 | // see http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/function/Function.java |
我们先看两组例子:
1 | // string concatenation |
我们可以发现,这两组操作其实有着相同的模式:
zero
,例子中分别是空串
和0
op
,例子中分别是concat
和+
op
满足结合律(associativity),即op(x, op(y, z)) == op(op(x, y), z)
op(zero, x) == op(x, zero) == x
那么我们可以这样表示monoid:
1 | trait Monoid[A] { |
事实上,monoid是十分普遍的,比如:
&&
,单位元是true
Integer
的最大值max
,单位元是Integer.MIN_VALUE
List
的连接,单位元是Nil(空列表)
A => A
的函数)的组合compose
,单位元是id
第5个并不是那么显而易见,实现如下:
1 | def endoMonoid[A]: Monoid[A => A] = new Monoid[A => A] { |
那么monoid是什么?
一个可结合的二元操作符和一个单位元元素,构成一个monoid。
Monoid是一种抽象,我们总是可以通过抽象来写出更加通用的代码:事实上,我们可以不关心monoid里的具体类型,直接写出可以对任何monoid都有效的代码。
事实上,对于monoid,我们并不关心计算发生的顺序,结合律保证了结果的一致:
1 | // 1. foldRight |
我们可以用任何顺序进行计算,甚至直接将每个任务分派给不同节点开始并行计算!
仔细观察,你会发现当我们对一个List
进行fold
操作(aka reduce
in JavaScript/Python)时,传给fold的操作,恰好是monoid的op
和zero
:
1 | List(1,2,3,4,5).foldLeft(0)((x, y) => x + y) == |
所以可以写一个更加通用的函数,接收一个List[A]
和一个Monoid[A]
,并用这个monoid去折叠:
1 | def concatenate[A](as: List[A], m: Monoid[A]): A = |
甚至可以将List[A]
map 成List[B]
:
1 | def foldMap[A, B](as: List[A], m: Monoid[B])(f: A => B): B = |
事实上,对于所有的可折叠数据结构,我们都可以忽略其具体结构,直接用monoid进行折叠。
比如,我们有一个存放了Int
的可折叠结构,计算其总和:
1 | ints.foldRight(intAddtionMonoid.zero)(intAddtionMonoid.op) |
我们并不关心,这个ints,究竟是List
还是Array
还是Vector
,甚至可能是Tree
或者Stream
!
对于这些结构,我们给它起个名字,叫做Foldable
,我们可以抽象成这样:
1 | trait Foldable[F[_]] { |
PS:F[_]
代表一个F是一个类型构造器,接收一个类型参数。比如,F[A]
可以具象成List[Int]
,Stream[String]
等等。
若类型A
和B
是monoid,那么tuple类型(A, B)
同样也是monoid。
我们只需要将A和B的op
和zero
组合成tuple
就可以了。
1 | // am: Monoid[A], bm: Monoid[B] |
There are monoids for all the following:
· approximate sets such as the Bloom filter;
· set cardinality estimators, such as the HyperLogLog algorithm;
· vectors and vector operations like stochastic gradient descent;
· quantile estimators such as the t-digest
to name but a few.– Chapter 9, Scala with Cats
在Lambda演算中,函数是没有名字的(都是匿名函数),那么如果函数没有名字,也就无法在函数体内显式地调用自身,也就无法定义递归函数,Y combinator就是用来解决这个问题的。
这篇文章想抛开那些数学概念,用程序语言(Scheme)的形式来讲解我们是如何推导出Y combinator的。
运行环境:
IDE:DrRacket
我们以递归函数length
为例,length
函数接受一个list,返回list的元素数目。
1 | (define length |
若你熟悉Scheme函数定义,可以跳过本小结后面部分。
(define length
,define
表达式将一个表达式(表达式即可能是函数也可能是值,在Scheme中并不区分这两点)绑定至一个名字,形式如同(define name func)
,之后便可以使用name
调用func
这个函数。在本定义中,func
是后面整个用括号包围的lambda
表达式,也就是2-4行。(lambda args body)
,lambda
表达式定义了一个匿名函数,表达式中第二个元素是参数列表,第三个元素是函数体。也就是说,这个匿名函数接受一个参数lst
,然后对函数体进行求值。(if condition then else)
,if
表达式,若condition
为真,则求值then
,若condition
为假,则求值else
。null?
接受一个list,判断是否为空。add1
接受一个值,将其加1后返回,定义如下:
1 | (define add1 |
rest
返回lst
去掉第一个元素的剩余部分,其实就是(define rest cdr)
。
也就是说,length
函数接受一个list,若list为空,则返回0
,若list非空,则求去掉第一个元素的list长度,并将其加一。求值过程如下:
1 | (length `(1 2)) ; `(1 2) is a list has two elements |
如果我们去掉define
,拿出其中的函数定义,是这样。
1 | (lambda (lst) |
**WAIT!!**既然现在没有了length
,显然我们也无法在函数体内调用length
了。
那么我们应该把length
换成什么呢?
我也不知道。
那么我们先试一试换成别的吧。
假设我们现在有一个函数id
,这个函数什么都不做,只接受一个参数并将其返回,定义如下。
1 | (define id |
那么,我们把id
丢进length
定义里面试试:
1 | ; length0 |
看上去是个奇怪的函数,这个函数能工作吗?
我们可以试一试。
1 | > ((lambda (lst) |
我们喂给这个奇怪的函数一个空list,它居然能返回正确的值!
具体原因,请读者先自己思考一下(笑
。
。
。
很简单,因为(null? lst)
为真,所以if
表达式直接返回了0
,压根没有对(add1 (id (rest lst)))
进行求值。
那么我们如果喂(apple)
呢,显然这个奇怪的函数并不能返回正确的长度,因为(id (rest lst))
无法给出“去掉第一个值的list的长度”。
总结一下,这个奇怪的函数:
那么,我们将这个函数叫做length0
,代表它对长度为0及0以下(并不存在)的list可以正常工作。
好了,我们现在有一个length0
,可以求出空表的长度。
那么下面这个函数有什么作用呢?
1 | ; length1 |
由于(length0 (rest lst))
是可以正确计算的,所以上面这个函数,可以计算长度小于等于1的list的长度。
你可能会问,但是我们没有length0
这个名字呀?
没关系,我们将length0
代换成上一小结的匿名函数形式,如下:
1 | ; length1 |
那么这个函数,我们就可以把它叫作length1
了,因为它可以正确求出长度小于等于1的list的长度!
那么,length2
就再来一次啦,把length1
丢进那个定义,也不需要显式使用length2
啦!
1 | ; length2 |
试一试length2
!
1 | > ((lambda (lst) |
真棒!现在能解决长度小于等于2的list了!
依此类推,我们能定义出length3
、length4
、length5
,甚至length100
、length1000
、length10000
了!
那么如果我们能写出length∞
,不就得到了我们的原始length
函数了吗,因为length
是可以处理任意长度的list的函数!
但是,我们没办法写出length∞
,因为无穷行代码哪里也放不下啊!(笑
试着观察length0
length1
length2
,我们会发现,这三个函数都遵循相同的形式,只有last-length
这个地方是需要改变的:
1 | ; same pattern |
根据DRY原则,我们为什么不把相同的形式给抽象出来呢?
我们可以写出一个函数,接受一个参数last-length
,然后就可以利用这个函数生成之后的length[i]
了。
就像这样:
1 | ; abstract-length |
这个函数接受参数last-length
,然后就会返回一个新的函数,可以比last-length
多处理一个长度的list。
那么,length0
可以表示为:
1 | ; length0 |
那么同理,length1
可以表示为:
1 | ; length1 |
当然,我们并没有length0
,需要将其转换为匿名形式,也就是这样:
1 | ; length1 |
那么length2
?
容易,再来一次:
1 | ; length2 |
**WAIT!!!**这难道不是依然在重复增加代码吗?
好吧,其实我们还是有所推进,别急,慢慢来。
按照我们刚刚的抽象,abstract-length
:
1 | ; abstract-length |
对于这个函数,我们如果:
id
,即(abstract-length id)
,那么它会返回length0
length[i]
,即(abstract-length length[i])
,那么它会返回length[i+1]
length[i]
,不妨将其称之为make-length
函数。last-length
的抽象:1 | ; length0 |
依然运用我们的old trick,将abstract-length
写成匿名形式:
1 | ; length0 |
好了,虽然这个形式看上去很复杂,但是它就是我们熟悉的length0
!
那么,length1
呢?
很简单,我们只需要将length0
喂给我们的abstract-length
,就会得到length1
。也就是将length0
中的id
替换成length0
:
1 | ; length1 |
换成匿名表达:
1 | ; length1 |
再进一步,length2
:
1 | ; length2 |
那,length3
?
1 | ; length3 |
好了好了,就不再继续演示了。
那么回顾一下我们的新表示方法,好像它们之间的差别,仅仅在于对参数id
应用了多少次make-length
函数:
length0
: (make-length id)length1
: (make-length (make-length id))length2
: (make-length (make-length (make-length id)))length3
: (make-length (make-length (make-length (make-length id))))是不是有点类似汉诺塔?
事实上,每当我们使用length
函数,我们并不会给它一个无限长的list。
那么,只要我们用一个足够大的length[i]
,可不可行呢?按照我们上一小节的抽象,我们可以轻易地写出length100
,甚至length10000000000000000
。
问题在于,这个足够大的数,仍然可能不够大。
那么什么时候我们会知道,这个数不够大呢?
显然,当我们需要用到最最最最内层的函数id
,也就是计算(id (rest lst))
的时候,就代表,这个数不够大了。
**注意:**我们喂给length2
一个长度小于等于2的list时,id
的函数应用是没有求值的。
那么,如果每当我们需要对id
的函数应用求值的时候,我们再对其应用一次make-length
,不就可以避免对id
的函数应用求值了吗?
那么,如果我们从不对id
的函数应用求值,也就是说,id
根本没什么用,我们可以把任意函数传递给最内层的make-length
。
那么,为何不试一试把make-length
作为最内层的参数传给make-length
呢。
那么,length0
:
1 | ; length0 |
回过头来看,其实last-length
也是接受一个参数length[i]
,然后返回新的函数length[i+1]
。
那不是和make-length
一样了?
是这样。
那我们将last-length
改名为make-length
也可以吗?
可以。只要保持函数参数和函数体内的名字一致即可。
那我们改名吧:
1 | ; length0 |
进一步,依照刚刚所说,我们只需要将length1
中最内层的函数应用添加一层
1 | ; length1 |
等等?为什么id
又出现了?
因为我们只能处理长度小于等于1的list。
如何解决此困境?
再次将make-length
传递给自身,这样每当我们需要更长的length[i]
的时候,就会自动将函数的处理能力+1。
就像这样:
1 | ; length1 |
这个函数的工作机理是,它展开的层数总是和喂给它的list
长度一样多。因为一旦层数不够长,他就会将make-length
应用于自身,增加多一次的处理。
试一试length1
能否工作:
1 | > (((lambda (make-length) |
很好,我们得到了一个能工作的length1
,事实上,这已经不再是length1
了,因为它能处理长度为2的list!甚至更长的!
这就和我们最初的length
一样了!
(如果你不明白这里的length1
为何能工作,可以试着再读一读这两个小节,或者手动推演一下这个lambda式子)
对比一下我们的显式递归length
和newlength
:
1 | ; length |
这两个定义在形式上还是有差别,因为前者定义中的(length (rest lst))
,在后者定义中变成了((make-length make-length) (rest lst))
。
为了使它们表现的一致,我们将(make-length make-length)
抽象出来就好了,然后作为参数传进去,而且我们将参数取名为length
!
1 | ; length |
好了,看上去大功告成,可以把两个定义在形式上相同的地方抽象出来,这样就可以用在别的递归函数定义中了。
WAIT!还没完,我们试着用一用newlength
,试着对(apple)
求值:
1 | > (((lambda (make-length) |
我们需要将这个表达式的值求出来,然后应用到(apple)
上
1 | ((lambda (make-length) |
首先我们需要将(lambda (make-length) (make-length make-length))
应用于后面那个式子(记作A)上,得到(A A)
,即:
1 | ( |
然后继续求值,将B应用于C,也就是将B的参数make-length
用C进行代换,得到:
1 |
|
现在我们得到了形如(D E)
的式子,然后我们继续求值,你会注意到,E的形状和刚刚我们得到的(A A)
形状是一样的,所以我们会在这里一直循环下去。
这就很奇怪了,我们在把(make-length make-length)
抽象出来之前函数都是好的呀?
回想一下,make-length
的作用是生成一个新的length[i+1]
函数,然后将其应用于一个list。
但是我们把(make-length make-length)
抽象出来之后,我们无法得到这个length[i+1]
函数,因为Scheme是应用序求值策略,make-length
反复对自身进行应用。
因此,我们要想一个办法,先让make-length
的应用停一停,等到需要它对自身进行应用的时候,再继续。
这个方法并不复杂,就是将(make-length make-length)
包裹在一个lambda
表达式之中,即(lambda (x) ((make-length make-length) x))
。
1 | ; newlength |
然后我们再把它抽象出来。
1 | ; new newlength |
这样,我们就得能正确使用的newlength
了,就能继续我们的抽象工作了。
对比一下newlength
和原始length
,使用“;;;;;;”分隔开的部分,完全就是原始length
的定义,除了将原始length
定义中的define
替换成lambda
。而且这部分,和make-length
完全无关!
那么,我们将这部分抽象出来,使其变成一个函数应用。
1 | ( |
现在我们有了两部分,Y和F:
length
具有相同形式的函数,然后返回一个递归函数length
;length
形式相同的匿名函数啦。那么这个Y部分,就是我们的Y combinator!一般被称作应用序Y combinator。
化简一下:
1 | ; applicative-order Y combinator |
有了Y combinator,我们就能从匿名函数中定义递归函数了!
你能写出一个简单的阶乘函数factorial
,然后用Y combinator将其变成匿名递归函数吗?
第九章的习题暂时跳过了,先更第十章。
开学了,事情多了起来,还要找工作,加把劲最近把这本书刷完吧!:P
其实从第8章开始,这本书对于monad就讲的太少,过几天这本书要出第二版,希望能在这方面改进改进。。。
我下单了一本《Haskell趣学指南》,打算结合起来看,然后再补上跳过的习题。
1 | data Nat = Zero | Succ Nat |
occurs :: Int -> Tree -> Bool
需要使用标准库data Ordering = LT | EQ | GT
, 以及compare :: Ord => a -> a -> Ordering
。
1 | data Tree = Leaf Int | Node Tree Int Tree |
balanced :: Tree -> Bool
平衡树:左、右子树的叶子数量相差不超过一个
1 | data Tree1 = Leaf1 Int | Node1 Tree1 Tree1 |
思路也蛮简单,把list分成两半,然后递归就成了。
1 | balance :: [Int] -> Tree1 |
\/
(disjunction)和<=>
(equivalence)运算符1 | type Assoc k v = [(k, v)] |
1 | -- 7 abstract machine |
这章信息量简直爆炸,书上给的东西太少了,讲的又太多了。找了一堆资料看了好久才弄明白。
结合的参考资料如下:
Chapter8讲课视频
Monadic Parsing in Haskell
“Programming In Haskell” error in sat function
按照书上的Parser
定义,是没法使用do notation的,所以下面的习题全部用>>=
完成。
完整的代码我放在这里。
int :: Parser Int
1 | int :: Parser Int |
comment :: Parser ()
1 | comment :: Parser () |
这里一个坑就是在于处理8 / 2 / 2 / 2
,1 - 2 - 3 - 4
这种情况,如果模仿+
, *
,的实现,很错误地容易把8 / 2 / 2 / 2
parse成(8 / 2) / (2 / 2)
。
1 |
|
expr ::= expr - nat | nat
的parser要注意的点在和上一题是一样的.
1 | -- expr' ::= expr' - nat | nat |
map
和filter
表示[f x | x <- xs, p x]
1 | -- [f x | x <- xs, p x] |
all
, any
, takeWhile
, dropWhile
1 | myall :: (a -> Bool) -> [a] -> Bool |
foldr
定义map f
和filter p
1 | mymap f = foldr (\x xs -> f x : xs) [] |
foldl
定义dec2int :: [Int] -> Int
1 | dec2int :: [Int] -> Int |
sumsqreven = compose [sum, map (^2), filter even]
1 | compose = foldr (.) id |
curry
& uncurry
1 | -- 6 |
unfold
定义chop8
, map f
, iterate f
1 | unfold p h t x | p x = [] |
我这里是在数据位的末端添加的,奇数个1则为1,否则为0。
1 | -- 8 |
这一章节就是在讲递归,所以下面的定义都是默认用递归定义。
1 | mypow :: Int -> Int -> Int |
length
, drop
, init
的递归求值过程1 |
|
1 | myand :: [Bool] -> Bool |
1 | myconcat :: [[a]] -> [a] |
1 | myreplicate :: Int -> a -> [a] |
1 | mynth :: [a] -> Int -> a |
1 | myelem :: Eq a => a -> [a] -> Bool |
merge
函数merge: 将两个有序list合并成一个有序list
1 | merge :: Ord a => [a] -> [a] -> [a] |
merge
定义归并排序msort
1 | halve :: [a] -> ([a], [a]) |
sum
, take
, last
1 |
|
1 | sum [x^2 | x <- [1..100]] |
replicate
1 | -- > replicate 3 True |
pyths
pyths n
返回满足x^2 + y^2 == z^2
的三元组(x, y, z)
,其中xyz都小于等于n
1 | -- > paths 10 |
perfects
完美数的定义:一个数所有的真因子(除掉自身以外的因子)之和等于它本身。
例如:6 = 1 + 2 + 3
1 | factors :: Int -> [Int] |
深入阅读:Perfect number
[(x, y) | x <- [1..3], y <- [1..3]]
将两层List Comprehension嵌套起来
1 | concat [[(x,y)| x <- [1..3] ]| y <- [1..3]] |
find
函数重新定义positions
函数1 | positions :: Eq a => a -> [a] -> [Int] |
scalarproduct
1 | scalarproduct :: [Int] -> [Int] -> Int |
思路是:
shift
进行移位的时候,将大写字母、小写字母分开处理;1 | let2int :: Char -> Int |
test:
1 | *Main> :l ch5ex.hs |
1 | halve :: [a] -> ([a], [a]) |
1 | -- conditional expression |
1 | -- 1 all pattern matching |
1 | mc1 a b = if a == False then False else |
1 | mc2 a b = if a == True then b else False |
mult x y z = x * y * z
1 | mult x y z == \x -> (\y -> (\z -> x * y * z)) |
1 | ['a', 'b', 'c'] -- :: [Char] |
1 | second xs = head (tail xs) -- [a] -> a |
第二题中,有几个函数用:t
查看的type是用t
t1
表示的,和a
b
是一个意思。
Eq class
的实例(instance)”是不可行的?题目中给出的两个函数相等的定义是这样:
显然,编译器不可能对两个函数测试所有的参数来判断它们是否相等。因为可能的参数是有无穷多个的。
]]>在Package Control中安装SublimeHaskell。
插件安装完成后,重启Sublime报错,提示需要安装hsdev cabal package。
1 | SublimeHaskell: hsdev executable couldn't be found! |
用cabal安装之:
1 | $ cabal install hsdev |
然后是cabal运行过程中报错,提示缺少happy cabal package。
1 | $ cabal install hsdev |
遂先装happy,再来装hsdev。
(PS: 这个hsdev包安装过程中的各种build超超超慢)
1 | $ cabal install happy |
等了好久终于装好了,结果打开Sublime依然报错,还需要手动配置SublimeHaskell。
菜单栏选Preferences -> Package Settings -> SublimeHaskell -> Settings - User
进行配置,文件内容如下:
1 | { |
注意:文件中的路径应当修改为你自己的路径,可以用cat ~/.cabal/where-is-my-stuff.txt
查看。
再次重启sublime,这次没有报错。
在Package Control中安装SublimeREPL。
这个插件可以直接新建一个tab用来跑GHCi,配合View -> Layout -> Coloum: 2
使用,方便开发与调试。
有待研究。
Tools -> Build System -> Automatic 或 Haskell
直接运行当前文件。
显示声明位置:f12
详细声明文档:ctrl+k ctrl+i
显示类型:ctrl+k ctrl+h ctrl+t
插入类型:ctrl+k ctrl+i ctrl+i
当然,也可以用command+shift+p
调用这些命令。
不用额外配置,通过Tools -> SublimeREPL -> Haskell
可以直接打开GHCi。
生命不息,折腾不止,终于可以愉快地敲haskell啦~
主要的坑都在cabal上,cabal这个包管理比起 pip npm 啥的实在太弱了,连uninstall都没有!(敲cabal uninstall
会告诉你以后会加上这个功能的。。。)
按照运算符优先级就可以,略过。
将length xs
用括号括起来即可。
last
last
: 返回非空list的最后一个元素
1 | mylast a = a !! ((length a) - 1) |
init
init
: 删除非空list的最后一个元素,并返回list
1 | myinit1 a = take ((length a) - 1) a |
用hexo d
命令部署的时候总是会卡住很久,反复尝试了几次后,每次都是control+C告终。
1 | ▶ hexo d |
hexo d -debug
查看日志发现信息一直在循环
1 | ▶ hexo deploy -debug |
首先检查_config.yml文件,看看deployment相关配置写对了没
1 | # Deployment |
对照官方文档后,发现配置并没有错,推测是ssh的问题。
将repo改成https地址后,问题依旧存在。
找了一些类似问题的资料后,尝试了删除.deploy_git
,更新git版本后,问题依然没有解决。
但是在删除.deploy_git
后,发现总是在gitlog后面卡住,推测是git push
的问题,尝试让git走代理,问题解决,令人哭笑不得。
git配置如下:
1 | git config --global http.proxy 'socks5://127.0.0.1:1080' |
设置的意思是让http/https协议走代理,即Shadowsocks的本地代理。
double (double 2)
的另外一种可能解释。书上给的例子是应用序和正则序(从左向右),可以将正则序(从右向左)当作答案。
1 | double (double 2) |
sum [x]
的求值过程。sum的定义:
1 | sum [] = 0 |
sum [x] 求值过程:
1 | sum [x] |
product
。仿照sum的定义,将product定义如下:
1 | product [] = 1 |
product [2, 3, 4]求值过程:
1 | product [2,3,4] |
qsort
改成输出倒序的。将课本中的例子larger和smaller交换即可。
1 | -- qsort.hs |
qsort
定义中的<=
换成<
会如何。若将<=
换成<
,则与x
相等的元素将会丢失。
1 | *Main> qsort [2,2,3,3,1] |
1 | this |