精华内容
下载资源
问答
  • 诺曼....其中清教徒的个人主义思想是美国自由主义思想的历史渊源,萨特的存在主义则为美国的自由主义思想提供了理论基础,作者本人的犹太出身和个性特征也成了《裸者与死者》自由主义思想的重要源泉。
  • 如何从根源上解决 HDFS 小文件问题

    千次阅读 2018-10-16 10:43:28
    我们知道,HDFS 被设计成...而这些数据的元数据(比如文件由哪些块组成、这些块分别存储在哪些节点)全部是由 NameNode 节点维护,为了达到高效的访问, NameNode 在启动的时候会将这些元数据全部加载到内存...

    原文地址:https://www.iteblog.com/archives/2320.html

    我们知道,HDFS 被设计成存储大规模的数据集,我们可以在 HDFS 上存储 TB 甚至 PB 级别的海量数据。而这些数据的元数据(比如文件由哪些块组成、这些块分别存储在哪些节点上)全部都是由 NameNode 节点维护,为了达到高效的访问, NameNode 在启动的时候会将这些元数据全部加载到内存中。而 HDFS 中的每一个文件、目录以及文件块,在 NameNode 内存都会有记录,每一条信息大约占用150字节的内存空间。由此可见,HDFS 上存在大量的小文件(这里说的小文件是指文件大小要比一个 HDFS 块大小(在 Hadoop1.x 的时候默认块大小64M,可以通过 dfs.blocksize 来设置;但是到了 Hadoop 2.x 的时候默认块大小为128MB了,可以通过 dfs.block.size 设置) 小得多的文件。)至少会产生以下几个负面影响:

    • 大量小文件的存在势必占用大量的 NameNode 内存,从而影响 HDFS 的横向扩展能力。
    • 另一方面,如果我们使用 MapReduce 任务来处理这些小文件,因为每个 Map 会处理一个 HDFS 块;这会导致程序启动大量的 Map 来处理这些小文件,虽然这些小文件总的大小并非很大,却占用了集群的大量资源!

    以上两个负面影响都不是我们想看见的。那么这么多的小文件一般在什么情况下产生?我在这里归纳为以下几种情况:

    • 实时流处理:比如我们使用 Spark Streaming 从外部数据源接收数据,然后经过 ETL 处理之后存储到 HDFS 中。这种情况下在每个 Job 中会产生大量的小文件。
    • MapReduce 产生:我们使用 Hive 查询一张含有海量数据的表,然后存储在另外一张表中,而这个查询只有简单的过滤条件(比如 select * from iteblog where from = 'hadoop'),这种情况只会启动大量的 Map 来处理,这种情况可能会产生大量的小文件。也可能 Reduce 设置不合理,产生大量的小文件,
    • 数据本身的特点:比如我们在 HDFS 上存储大量的图片、短视频、短音频等文件,由于这些文件的特点,而且数量众多,也可能给 HDFS 大量灾难。

    那么针对这些小文件,现有哪几种解决方案呢?

    现有小文件解决方案

    在本博客的《Hadoop小文件优化》文章中,翻译了 Cloudera 官方技术博客的《The Small Files Problem》文章,里面提供了两种 HDFS 小文件的解决方案。

    HAR files

    Hadoop Archives (HAR files)是在 Hadoop 0.18.0 版本中引入的,它的出现就是为了缓解大量小文件消耗 NameNode 内存的问题。HAR 文件是通过在 HDFS 上构建一个层次化的文件系统来工作。一个 HAR 文件是通过 hadoop 的 archive 命令来创建,而这个命令实 际上也是运行了一个 MapReduce 任务来将小文件打包成 HAR 文件。对客户端来说,使用 HAR 文件没有任何影响。所有的原始文件都可见并且可访问的(通过 har://URL)。但在 HDFS 端它内部的文件数减少了。架构如下:


    如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop

    从上面实现图我们可以看出,Hadoop 在进行最终文件的读取时,需要先访问索引数据,所以在效率上会比直接读取 HDFS 文件慢一些。

    Sequence Files

    第二种解决小文件的方法是使用 SequenceFile。这种方法使用小文件名作为 key,并且文件内容作为 value,实践中这种方式非常管用。如下图所示:


    如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop

    和 HAR 不同的是,这种方式还支持压缩。该方案对于小文件的存取都比较自由,不限制用户和文件的多少,但是 SequenceFile 文件不能追加写入,适用于一次性写入大量小文件的操作。

    HBase

    除了上面的方法,其实我们还可以将小文件存储到类似于 HBase 的 KV 数据库里面,也可以将 Key 设置为小文件的文件名,Value 设置为小文件的内容,相比使用 SequenceFile 存储小文件,使用 HBase 的时候我们可以对文件进行修改,甚至能拿到所有的历史修改版本。

    从 HDFS 底层解决小文件问题

    以上三种方法虽然能够解决小文件的问题,但是这些方法都有局限:HAR Files 和 Sequence Files 一旦创建,之后都不支持修改,所以这是对读场景很友好的;而使用 HBase 需要引入外部系统,维护成本很高。最后,这些方法都没有从根本上解决。那么能不能从 HDFS 底层解决这个问题呢?也就是对使用方来说,我们不需要考虑写的文件大小。目前 Hadoop 社区确实有很多相应的讨论和方案设想。下面将简要进行描述。

    HDFS-8998

    我们都知道,一个文件在 HDFS 上对应一个或多个 Block,每个 Block 在 NameNode (INode 和 BlocksMap)中都存在一定的元数据,而且这些数据需要占用 NameNode 一定内存。所以说,如果 HDFS 中存在大量的小文件,因为这些小文件都是小于一个 Block 大小,所以这些文件占用了一个 Block;这样,海量的小文件占用了海量的 Block 。那我们能不能把这些小 Block 合并成一个大 Block?这正是 HDFS-8998 的思想。其核心实现如下:

    • 用户可以在 HDFS 上指定一个区域,用于存放小文件,这个区域称为小文件区域(Small file zone);
    • NameNode 将保存固定大小的 block 列表,列表的大小是可以配置的,比如我们配置成 M;
    • 当客户端1第一次向 NameNode 发出写时,NameNode 将为客户端1创建第一个 blockid,并锁定此块;只有在关闭 OutputStream 的时候才释放这个锁;
    • 当客户端2向 NameNode 发出写时,NameNode 将尝试为其分配未锁定的块(unlocked block),如果没有未锁定的块,并且现有的块数小于之前配置的大小(M),这时候 NameNode 则为客户端2创建新的 blockid 并锁定该块。
    • 其余的客户端操作和这个类似;
    • 客户端写数据的操作都是将数据追加到获取到的块上;
    • 如果某个块被写满,也会分配新的一个块给客户端,然后写满的块将从 M 个候选块列表中移除,表示此块将不再接受写处理。
    • 当 M 个块中没有未锁住的块并且 NameNode 无法再申请新块的时候,则当前客户端必须等待其它客户端操作完毕,并释放块。


    如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop

    从上图可以看出,每个 block 同时只能由一个客户端处理,但是当这个客户端写完,并释放相关锁之后,还能由其他客户端复用!直到这个 block 达到 HDFS 配置的快大小(比如 128MB)。

    从上面的阐述可以看出,一个 block 将包含多个文件,那么我们需要引入额外的服务来维护各个文件在 block 中的偏移量。其余的读写删操作如下:

    • 读取:关于这些文件的读取,其实和读取正常的 HDFS 文件类似。
    • 删除:因为现在一个 block 包含不止一个文件,所以删除操作不能直接删除一个 block。现在的删除操作是:从 NameNode 中的 BlocksMap 删除 INode;然后当这个块中被删除的数据达到一定的阈值(这个阈值是可以配置的) ,对应的块对象会被重写。
    • append 和 truncate:对小文件的 truncate 和 append 是不支持的,因为这些操作代价非常高。

    HDFS-8286

    HDFS-8998 的设计目标是直接从底层的 block 做一些修改,从而减少文件元数据的条数,以此来减少 NN 的内存消耗。而 HDFS-8286 的目标是直接从解决 NN 管理的元数据入手,将 NN 管理的元数据从保存在内存转向到保存在第三方 KV 存储系统中,以此减缓 NN 的内存使用。更进一步的讲,这种方法同时也提高了 Hadoop 集群的扩展性。

    现在的 HDFS 是以层次结构的形式来管理文件和目录的,所有的文件和目录都表示为 inode 对象。为了实现层次结构,一个目录需要包含对其所有子文件的引用。而 HDFS-8286 的方案采用 KV 的形式来存储元数据。下面我们来看看它是怎么实现的,我们先来了解下两条 kv 对应的规则:

    • 每个 INode 对象都有对应的 inode id
    • key 为 <pid ,foo> 将直接映射到 foo 对象的 inode id,而且 foo 对象的父对象的 inode id 为 pid

    根据这两条规则,现在我们想基于 KV 元数据结构获取 /foo/bar 路径,解析过程如下图所示:


    如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop

    • 首先,root 的 inode id 为1,那么系统会将 foo 对象和其父对象的 inode id 进行组合,并得到一个 key <1,foo>;
    • 第二步,系统根据上面得到的 key <1,foo>,从 KV 存储里面拿到 foo 对象的 inode id。从上图可以看出, foo 对象对应的 inode id 为2,对应上图的步骤 i;
    • 第三步和第二步类似,系统需要拿到 bar 对象的 inode id,同样也是构造一个k key,得到 <2,bar>,最后从 KV 存储里面拿到 bar 对象的 inode id,这里为 3,对应上图的步骤 ii;
    • 最后,系统直接根据 inode id 为 3,从 KV 存储里面拿到对应的 bar 的内容,对应图中的步骤 iii。

    这个过程可以看到需要从 KV 存储里面进行多次检索,并进行解析,可能会在这里面出现一些性能问题。

    Hadoop Ozone

    Ozone 是 Hortonworks 基于 HDFS 实现的一个对象存储服务,旨在基于 HDFS 的 DataNode 存储,支持更大规模的数据对象存储,支持各种对象大小并且拥有 HDFS 的可靠性,一致性和可用性,对应的 issue 请参见 HDFS-7240。目前这个项目已经成为 Apache Hadoop 的子项目,参见 ozone。Ozone 的一大目标就是扩展 HDFS,使其支持数十亿个对象的存储。关于 Ozone 的使用文档可以参见 Apache Hadoop Ozone

    总结

    社区关于 HDFS 的扩展性问题基本上都是通过解决 NameNode 元数据的存储问题,将元数据由原来的单节点存储扩展到多个;甚至直接使用外部的 KV 系统来存储一部分元数据或全部的元数据。相信不久的将来,使用 HDFS 存储小文件已经不是什么问题了。

    展开全文
  • Lisp之根源

    千次阅读 2012-10-19 07:14:12
    Lisp之根源 保罗格雷厄姆 约翰麦卡锡于1960年发表了一篇非凡的论文,他在这篇论文中对编程的贡献有如欧几里德对几何的贡献.1 他向我们展示了,在只给定几个简单的操作符和一个表示函数的记号的基础, 如何...

    Lisp之根源

    保罗格雷厄姆

    约翰麦卡锡于1960年发表了一篇非凡的论文,他在这篇论文中对编程的贡献有如欧几里德对几何的贡献.1 他向我们展示了,在只给定几个简单的操作符和一个表示函数的记号的基础上, 如何构造出一个完整的编程语言. 麦卡锡称这种语言为Lisp, 意为List Processing, 因为他的主要思想之一是用一种简单的数据结构表(list)来代表代码和数据.

    值得注意的是,麦卡锡所作的发现,不仅是计算机史上划时代的大事, 而且是一种在我们这个时代编程越来越趋向的模式.我认为目前为止只有两种真正干净利落, 始终如一的编程模式:C语言模式和Lisp语言模式.此二者就象两座高地, 在它们中间是尤如沼泽的低地.随着计算机变得越来越强大,新开发的语言一直在坚定地趋向于Lisp模式. 二十年来,开发新编程语言的一个流行的秘决是,取C语言的计算模式,逐渐地往上加Lisp模式的特性,例如运行时类型和无用单元收集.

    在这篇文章中我尽可能用最简单的术语来解释约翰麦卡锡所做的发现. 关键是我们不仅要学习某个人四十年前得出的有趣理论结果, 而且展示编程语言的发展方向. Lisp的不同寻常之处--也就是它优质的定义--是它能够自己来编写自己. 为了理解约翰麦卡锡所表述的这个特点,我们将追溯他的步伐,并将他的数学标记转换成能够运行的Common Lisp代码.

    七个原始操作符

    开始我们先定义表达式.表达式或是一个原子(atom),它是一个字母序列(如 foo),或是一个由零个或多个表达式组成的(list), 表达式之间用空格分开, 放入一对括号中. 以下是一些表达式:

    foo
    ()
    (foo)
    (foo bar)
    (a b (c) d)
    
    最后一个表达式是由四个元素组成的表, 第三个元素本身是由一个元素组成的表.

    在算术中表达式 1 + 1 得出值2. 正确的Lisp表达式也有值. 如果表达式e得出值v,我们说e返回v. 下一步我们将定义几种表达式以及它们的返回值.

    如果一个表达式是表,我们称第一个元素为操作符,其余的元素为自变量.我们将定义七个原始(从公理的意义上说)操作符: quote,atom,eq,car,cdr,cons,和 cond.

    1. (quote x) 返回x.为了可读性我们把(quote x)简记 为'x.

      > (quote a)
      a
      > 'a
      a
      > (quote (a b c))
      (a b c)
      

    2. (atom x)返回原子t如果x的值是一个原子或是空表,否则返回(). 在Lisp中我们按惯例用原子t表示真, 而用空表表示假.

      > (atom 'a)
      t
      > (atom '(a b c))
      ()
      > (atom '())
      t
      

      既然有了一个自变量需要求值的操作符, 我们可以看一下quote的作用. 通过引用(quote)一个表,我们避免它被求值. 一个未被引用的表作为自变量传给象 atom这样的操作符将被视为代码:

      > (atom (atom 'a))
      t
      

      反之一个被引用的表仅被视为表, 在此例中就是有两个元素的表:

      > (atom '(atom 'a))
      ()
      

      这与我们在英语中使用引号的方式一致. Cambridge(剑桥)是一个位于麻萨诸塞州有90000人口的城镇. 而``Cambridge''是一个由9个字母组成的单词.

      引用看上去可能有点奇怪因为极少有其它语言有类似的概念. 它和Lisp最与众不同的特征紧密联系:代码和数据由相同的数据结构构成, 而我们用quote操作符来区分它们.

    3. (eq x y)返回t如果xy的值是同一个原子或都是空表, 否则返回().

      > (eq 'a 'a)
      t
      > (eq 'a 'b)
      ()
      > (eq '() '())
      t
      

    4. (car x)期望x的值是一个表并且返回x的第一个元素.

      > (car '(a b c))
      a
      

    5. (cdr x)期望x的值是一个表并且返回x的第一个元素之后的所有元素.
      > (cdr '(a b c))
      (b c)
      

    6. (cons x y)期望y的值是一个表并且返回一个新表,它的第一个元素是x的值, 后面跟着y的值的各个元素.

      > (cons 'a '(b c))
      (a b c)
      > (cons 'a (cons 'b (cons 'c '())))
      (a b c)
      > (car (cons 'a '(b c)))
      a
      > (cdr (cons 'a '(b c)))
      (b c)
      
    7. (cond ($p_{1}$...$e_{1}$) ...($p_{n}$...$e_{n}$)) 的求值规则如下. p表达式依次求值直到有一个返回t. 如果能找到这样的p表达式,相应的e表达式的值作为整个cond表达式的返回值.

      > (cond ((eq 'a 'b) 'first)
              ((atom 'a)  'second))
      second
      

      当表达式以七个原始操作符中的五个开头时,它的自变量总是要求值的.2 我们称这样 的操作符为函数.

    函数的表示

    接着我们定义一个记号来描述函数.函数表示为(lambda ( $p_{1}$... $p_{n}$) e),其中 $p_{1}$... $p_{n}$是原子(叫做 参数), e是表达式. 如果表达式的第一个元素形式如上

    ((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)

    则称为函数调用.它的值计算如下.每一个表达式$a_{i}$先求值,然后e再求值.在e的求值过程中,每个出现在e中的$p_{i}$的值是相应的$a_{i}$在最近一次的函数调用中的值.

    > ((lambda (x) (cons x '(b))) 'a)
    (a b)
    > ((lambda (x y) (cons x (cdr y)))
       'z
       '(a b c))
    (z b c)
    
    如果一个表达式的第一个元素 f是原子且 f不是原始操作符

    (f $a_{1}$...$a_{n}$)

    并且f的值是一个函数(lambda ($p_{1}$...$p_{n}$)),则以上表达式的值就是

    ((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)

    的值. 换句话说,参数在表达式中不但可以作为自变量也可以作为操作符使用:

    > ((lambda (f) (f '(b c)))
       '(lambda (x) (cons 'a x)))
    (a b c)
    

    有另外一个函数记号使得函数能提及它本身,这样我们就能方便地定义递归函数.3 记号

    (label f (lambda ($p_{1}$...$p_{n}$) e))

    表示一个象(lambda ($p_{1}$...$p_{n}$) e)那样的函数,加上这样的特性: 任何出现在e中的f将求值为此label表达式, 就好象f是此函数的参数.

    假设我们要定义函数(subst x y z), 它取表达式x,原子y和表z做参数,返回一个象z那样的表, 不过z中出现的y(在任何嵌套层次上)被x代替.

    > (subst 'm 'b '(a b (a b c) d))
    (a m (a m c) d)
    
    我们可以这样表示此函数
    (label subst (lambda (x y z)
                   (cond ((atom z)
                          (cond ((eq z y) x)
                                ('t z)))
                         ('t (cons (subst x y (car z))
                                   (subst x y (cdr z)))))))
    
    我们简记 f=(label f (lambda ( $p_{1}$... $p_{n}$) e))为

    (defun f ($p_{1}$...$p_{n}$) e)

    于是

    (defun subst (x y z)
      (cond ((atom z)
             (cond ((eq z y) x)
                   ('t z)))
            ('t (cons (subst x y (car z))
                      (subst x y (cdr z))))))
    
    偶然地我们在这儿看到如何写cond表达式的缺省子句. 第一个元素是't的子句总是会成功的. 于是

    (cond (x y) ('t z))

    等同于我们在某些语言中写的

    if x then y else z

    一些函数

    既然我们有了表示函数的方法,我们根据七个原始操作符来定义一些新的函数. 为了方便我们引进一些常见模式的简记法. 我们用c xr,其中 x是a或d的序列,来简记相应的car和cdr的组合. 比如(cadr e)是(car (cdr e))的简记,它返回 e的第二个元素.

    > (cadr '((a b) (c d) e))
    (c d)
    > (caddr '((a b) (c d) e))
    e
    > (cdar '((a b) (c d) e))
    (b)
    
    我们还用(list $e_{1}$... $e_{n}$)表示(cons $e_{1}$...(cons $e_{n}$'()) ...).
    > (cons 'a (cons 'b (cons 'c '())))
    (a b c)
    > (list 'a 'b 'c)
    (a b c)
    

    现在我们定义一些新函数. 我在函数名后面加了点,以区别函数和定义它们的原始函数,也避免与现存的common Lisp的函数冲突.

    1. (null. x)测试它的自变量是否是空表.

      (defun null. (x)
        (eq x '()))
      
      > (null. 'a)
      ()
      > (null. '())
      t
      

    2. (and. x y)返回t如果它的两个自变量都是t, 否则返回().

      (defun and. (x y)
        (cond (x (cond (y 't) ('t '())))
              ('t '())))
      
      > (and. (atom 'a) (eq 'a 'a))
      t
      > (and. (atom 'a) (eq 'a 'b))
      ()
      

    3. (not. x)返回t如果它的自变量返回(),返回()如果它的自变量返回t.

      (defun not. (x)
        (cond (x '())
              ('t 't)))
      
      > (not. (eq 'a 'a))
      ()
      > (not. (eq 'a 'b))
      t
      

    4. (append. x y)取两个表并返回它们的连结.

      (defun append. (x y)
         (cond ((null. x) y)
               ('t (cons (car x) (append. (cdr x) y)))))
      
      > (append. '(a b) '(c d))
      (a b c d)
      > (append. '() '(c d))
      (c d)
      

    5. (pair. x y)取两个相同长度的表,返回一个由双元素表构成的表,双元素表是相应位置的x,y的元素对.

      (defun pair. (x y)
        (cond ((and. (null. x) (null. y)) '())
              ((and. (not. (atom x)) (not. (atom y)))
               (cons (list (car x) (car y))
                     (pair. (cdr) (cdr y))))))
      
      > (pair. '(x y z) '(a b c))
      ((x a) (y b) (z c))
      

    6. (assoc. x y)取原子x和形如pair.函数所返回的表y,返回y中第一个符合如下条件的表的第二个元素:它的第一个元素是x.

      (defun assoc. (x y)
        (cond ((eq (caar y) x) (cadar y))
              ('t (assoc. x (cdr y)))))
      
      > (assoc. 'x '((x a) (y b)))
      a
      > (assoc. 'x '((x new) (x a) (y b)))
      new
      

    一个惊喜

    因此我们能够定义函数来连接表,替换表达式等等.也许算是一个优美的表示法, 那下一步呢? 现在惊喜来了. 我们可以写一个函数作为我们语言的解释器:此函数取任意Lisp表达式作自变量并返回它的值. 如下所示:

    (defun eval. (e a)
      (cond 
        ((atom e) (assoc. e a))
        ((atom (car e))
         (cond 
           ((eq (car e) 'quote) (cadr e))
           ((eq (car e) 'atom)  (atom   (eval. (cadr e) a)))
           ((eq (car e) 'eq)    (eq     (eval. (cadr e) a)
                                        (eval. (caddr e) a)))
           ((eq (car e) 'car)   (car    (eval. (cadr e) a)))
           ((eq (car e) 'cdr)   (cdr    (eval. (cadr e) a)))
           ((eq (car e) 'cons)  (cons   (eval. (cadr e) a)
                                        (eval. (caddr e) a)))
           ((eq (car e) 'cond)  (evcon. (cdr e) a))
           ('t (eval. (cons (assoc. (car e) a)
                            (cdr e))
                      a))))
        ((eq (caar e) 'label)
         (eval. (cons (caddar e) (cdr e))
                (cons (list (cadar e) (car e)) a)))
        ((eq (caar e) 'lambda)
         (eval. (caddar e)
                (append. (pair. (cadar e) (evlis. (cdr  e) a))
                         a)))))
    
    (defun evcon. (c a)
      (cond ((eval. (caar c) a)
             (eval. (cadar c) a))
            ('t (evcon. (cdr c) a))))
    
    (defun evlis. (m a)
      (cond ((null. m) '())
            ('t (cons (eval.  (car m) a)
                      (evlis. (cdr m) a)))))
    
    eval.的定义比我们以前看到的都要长. 让我们考虑它的每一部分是如何工作的.

    eval.有两个自变量: e是要求值的表达式, a是由一些赋给原子的值构成的表,这些值有点象函数调用中的参数. 这个形如pair.的返回值的表叫做环境. 正是为了构造和搜索这种表我们才写了pair.和assoc..

    eval.的骨架是一个有四个子句的cond表达式. 如何对表达式求值取决于它的类型. 第一个子句处理原子. 如果e是原子, 我们在环境中寻找它的值:

    > (eval. 'x '((x a) (y b)))
    a
    

    第二个子句是另一个cond, 它处理形如(a ...)的表达式, 其中a是原子. 这包括所有的原始操作符, 每个对应一条子句.

    > (eval. '(eq 'a 'a) '())
    t
    > (eval. '(cons x '(b c))
             '((x a) (y b)))
    (a b c)
    
    这几个子句(除了quote)都调用eval.来寻找自变量的值.

    最后两个子句更复杂些. 为了求cond表达式的值我们调用了一个叫 evcon.的辅助函数. 它递归地对cond子句进行求值,寻找第一个元素返回t的子句. 如果找到了这样的子句, 它返回此子句的第二个元素.

    > (eval. '(cond ((atom x) 'atom)
                    ('t 'list))
             '((x '(a b))))
    list
    

    第二个子句的最后部分处理函数调用. 它把原子替换为它的值(应该是lambda 或label表达式)然后对所得结果表达式求值. 于是

    (eval. '(f '(b c))
           '((f (lambda (x) (cons 'a x)))))
    
    变为
    (eval. '((lambda (x) (cons 'a x)) '(b c))
           '((f (lambda (x) (cons 'a x)))))
    
    它返回(a b c).

    eval.的最后cond两个子句处理第一个元素是lambda或label的函数调用.为了对label 表达式求值, 先把函数名和函数本身压入环境, 然后调用eval.对一个内部有 lambda的表达式求值. 即:

    (eval. '((label firstatom (lambda (x)
                                (cond ((atom x) x)
                                      ('t (firstatom (car x))))))
             y)
           '((y ((a b) (c d)))))
    
    变为
    (eval. '((lambda (x)
               (cond ((atom x) x)
                     ('t (firstatom (car x)))))
             y)
            '((firstatom
               (label firstatom (lambda (x)
                                (cond ((atom x) x)
                                      ('t (firstatom (car x)))))))
              (y ((a b) (c d)))))
    
    最终返回a.

    最后,对形如((lambda ($p_{1}$...$p_{n}$) e) $a_{1}$...$a_{n}$)的表达式求值,先调用evlis.来求得自变量($a_{1}$...$a_{n}$)对应的值($v_{1}$...$v_{n}$),把($p_{1}$$v_{1}$)...($p_{n}$$v_{n}$)添加到环境里, 然后对e求值. 于是

    (eval. '((lambda (x y) (cons x (cdr y)))
             'a
             '(b c d))
           '())
    
    变为
    (eval. '(cons x (cdr y))
           '((x a) (y (b c d))))
    
    最终返回(a c d).

    后果

    既然理解了eval是如何工作的, 让我们回过头考虑一下这意味着什么. 我们在这儿得到了一个非常优美的计算模型. 仅用quote,atom,eq,car,cdr,cons,和cond, 我们定义了函数eval.,它事实上实现了我们的语言,用它可以定义任何我们想要的额外的函数.

    当然早已有了各种计算模型--最著名的是图灵机. 但是图灵机程序难以读懂. 如果你要一种描述算法的语言, 你可能需要更抽象的, 而这就是约翰麦卡锡定义 Lisp的目标之一.

    约翰麦卡锡于1960年定义的语言还缺不少东西. 它没有副作用, 没有连续执行 (它得和副作用在一起才有用), 没有实际可用的数,4 没有动态可视域. 但这些限制可以令人惊讶地用极少的额外代码来补救. Steele和Sussman在一篇叫做``解释器的艺术''的著名论文中描述了如何做到这点.5

    如果你理解了约翰麦卡锡的eval, 那你就不仅仅是理解了程序语言历史中的一个阶段. 这些思想至今仍是Lisp的语义核心. 所以从某种意义上, 学习约翰麦卡锡的原著向我们展示了Lisp究竟是什么. 与其说Lisp是麦卡锡的设计,不如说是他的发现. 它不是生来就是一门用于人工智能, 快速原型开发或同等层次任务的语言. 它是你试图公理化计算的结果(之一).

    随着时间的推移, 中级语言, 即被中间层程序员使用的语言, 正一致地向Lisp靠近. 因此通过理解eval你正在明白将来的主流计算模式会是什么样.

    注释

    把约翰麦卡锡的记号翻译为代码的过程中我尽可能地少做改动. 我有过让代码更容易阅读的念头, 但是我还是想保持原汁原味.

    在约翰麦卡锡的论文中,假用f来表示, 而不是空表. 我用空表表示假以使例子能在Common Lisp中运行. (fixme)

    我略过了构造dotted pairs, 因为你不需要它来理解eval. 我也没有提apply, 虽然是apply(它的早期形式, 主要作用是引用自变量), 被约翰麦卡锡在1960年称为普遍函数, eval只是不过是被apply调用的子程序来完成所有的工作.

    我定义了list和cxr等作为简记法因为麦卡锡就是这么做的. 实际上 cxr等可以被定义为普通的函数. List也可以这样, 如果我们修改eval, 这很容易做到, 让函数可以接受任意数目的自变量.

    麦卡锡的论文中只有五个原始操作符. 他使用了cond和quote,但可能把它们作为他的元语言的一部分. 同样他也没有定义逻辑操作符and和not, 这不是个问题, 因为它们可以被定义成合适的函数.

    在eval.的定义中我们调用了其它函数如pair.和assoc.,但任何我们用原始操作符定义的函数调用都可以用eval.来代替. 即

    (assoc. (car e) a)
    
    能写成

    (eval. '((label assoc.
                    (lambda (x y)
                      (cond ((eq (caar y) x) (cadar y))
                            ('t (assoc. x (cdr y))))))
             (car e)
             a)
            (cons (list 'e e) (cons (list 'a a) a)))
    

    麦卡锡的eval有一个错误. 第16行是(相当于)(evlis. (cdr e) a)而不是(cdr e), 这使得自变量在一个有名函数的调用中被求值两次. 这显示当论文发表的时候, eval的这种描述还没有用IBM 704机器语言实现. 它还证明了如果不去运行程序, 要保证不管多短的程序的正确性是多么困难.

    我还在麦卡锡的论文中碰到一个问题. 在定义了eval之后, 他继续给出了一些更高级的函数--接受其它函数作为自变量的函数. 他定义了maplist:

    (label maplist
           (lambda (x f)
             (cond ((null x) '())
                   ('t (cons (f x) (maplist (cdr x) f))))))
    
    然后用它写了一个做微分的简单函数diff. 但是diff传给maplist一个用 x做参数的函数, 对它的引用被maplist中的参数x所捕获. 6

    这是关于动态可视域危险性的雄辩证据, 即使是最早的更高级函数的例子也因为它而出错. 可能麦卡锡在1960年还没有充分意识到动态可视域的含意. 动态可视域令人惊异地在Lisp实现中存在了相当长的时间--直到Sussman和Steele于 1975年开发了Scheme. 词法可视域没使eval的定义复杂多少, 却使编译器更难写了.

    About this document ...

    Lisp之根源

    This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)

    Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
    Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

    The command line arguments were:
    latex2html -split=0 roots_of_lisp.tex

    The translation was initiated by Dai Yuwen on 2003-10-24


    Footnotes

    ... 欧几里德对几何的贡献. 1
    ``Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part1.'' Communication of the ACM 3:4, April 1960, pp. 184-195.
    ...当表达式以七个原始操作符中的五个开头时,它的自变量总是要求值的. 2
    以另外两个操作符quote和cond开头的表达式以不同的方式求值. 当 quote表达式求值时, 它的自变量不被求值,而是作为整个表达式的值返回. 在 一个正确的cond表达式中, 只有L形路径上的子表达式会被求值.
    ... 数. 3
    逻辑上我们不需要为了这定义一个新的记号. 在现有的记号中用 一个叫做Y组合器的函数上的函数, 我们可以定义递归函数. 可能麦卡锡在写 这篇论文的时候还不知道Y组合器; 无论如何, label可读性更强.
    ... 没有实际可用的数, 4
    在麦卡锡的1960 年的Lisp中, 做算术是可能的, 比如用一个有n个原子的表表示数n.
    ... 的艺术''的著名论文中描述了如何做到这点. 5
    Guy Lewis Steele, Jr. and Gerald Jay Sussman, ``The Art of the Interpreter, or the Modularity Complex(Parts Zero,One,and Two),'' MIT AL Lab Memo 453, May 1978.
    ... 对它的引用被maplist中的参数x所捕获. 6
    当代的Lisp程序 员在这儿会用mapcar代替maplist. 这个例子解开了一个谜团: maplist为什 么会在Common Lisp中. 它是最早的映射函数, mapcar是后来增加的.

    next_inactive up previous
    Dai Yuwen 2003-10-24

    展开全文
  • 尽管提供详尽的讨论超出了本章的范围,但来自多种传统的领先思想家的快照使我们能够对现代协作消费系统进行概念化,并对其最佳设计和轨迹进行预测。 同时,这篇评论强调了任何一种观点未能完全解决当代协作消费的...
  • 当你遇到了问题,不管它是什么类型的什么方面的,切记!最终的根源都是因为人的问题。兵书上云:攻城为下,攻心为上。不战而屈人之兵是怎样的一个境界呀。 

    当你遇到了问题,不管它是什么类型的什么方面的,切记!最终的根源都是因为人的问题。兵书上云:攻城为下,攻心为上。不战而屈人之兵是怎样的一个境界呀。

     

    展开全文
  • RX系列一| ReactiveX根源 | 观察者模式分析Rx的响应式编程算是很火了,对吧,但是我的工作基本就不会接触,所以学习的比较晚,到现在才分享给大家,我们一点点的去学,当你看完这整个系列的博客的时候,我相信你最...

    RX系列一 | ReactiveX根源 | 观察者模式分析


    Rx的响应式编程算是很火了,对吧,但是我的工作基本上就不会接触,所以学习的比较晚,到现在才分享给大家,我们一点点的去学,当你看完这整个系列的博客的时候,我相信你最起码也是能把Rx的操作放入你的项目中,既然如此,那我们就开始吧!

    一.资料

    现在的资料都烂大街了,我也是看这些资料学的,先分享出来

    当然还有其他更多的优秀资源,这里就不一一罗列了

    二.Rx介绍

    ReactiveX这种思想可以理解为编程模型,全称是Reactive Extensions,微软在2012年11月开源的编程模型,其目的就是提供一致的编程接口,帮助开发者更方便的处理异步数据流,所以目前支持的还是比较多的,像RxJava/RxAndroid/RxJs等,说实话,ReactiveX可以说是一种编程思想的突破,他对我们的影响很大,很多人说Rx难用,但是你用习惯了,你就爱不释手了,就像Android Studio一样,刚开始是很多人用不习惯,后来就离不开他了。

    三.Rx模式

    说他的模式,很多人肯定知道了,那就是观察者模式啊,我就死死的盯着你

    • 创建:创建事件流和数据流

    • 组合:链式操作,Rx使用的查询式的操作符和变换数据流

    • 监听:Rx可以订阅任何可以被操作的数据流并且执行操作

    他的模型也对应的他的操作符,我列举一些

    • 过滤filter

    • 选择select

    • 变换transform

    • 结合combine

    • 组合compose

    四.优点

    优点可以概括为:代码写的少,逻辑理得好

    • 函数编程风格,可以很好的理清逻辑

    • 操作符:几行代码就可以处理复杂操作

    • 异步错误处理:异常有迹可循,归纳到位

    • 代码优化,处理同步,并发都是优点

    五.RxJava的观察者模式

    观察者模式面向的需求是:A对象(观察者)对B对象(被观察者)的某种高度变化敏感,需要在B变化的一瞬间做出反应,这点很类似警察抓小偷,警察一直在观察着小偷,当小偷开始偷东西的时候一下把他抓住

    我们本篇会将Android中RxAndroid的运用,在此之前,你要理解一下RxJava,放心,这对你来说并不是什么难事

    六.RxJava的概念

    RxJava的四个基本概念

    • Observable(被观察者)

    • Observer(观察者)

    • subscribe(订阅)

    • 事件

    由这四个概念就组成了最基本的观察者了,Observable和Observer通过subscribe订阅,从而让Observable需要的时候发出事件通知Observer

    七.JAVA事件监听

    我们可用先模拟一下我们之前的事件监听是怎么样的,其实就是模拟一下警察抓小偷,逻辑很简单,我写个小偷,写几个警察,当小偷偷东西的时候警察就抓他,那好,那我们怎么做?仔细看代码

    1.被观察者

    /*
     *项目名: RxSample
     *包名:   com.liuguilin.rxsample.java
     *文件名:  Watched
     *创建者:  LGL
     *创建时间:2016/12/1112:57
     *描述:    被观察者
     */
    public interface Watched {
        //添加观察者
        public void addWatcher(Watcher watcher);
        //取消观察者
        public void removeWatcher(Watcher watcher);
        //通知
        public void notifyWatcher(String string);
    }

    2.观察者

    /*
     *项目名: RxSample
     *包名:   com.liuguilin.rxsample.java
     *文件名:  Watcher
     *创建者:  LGL
     *创建时间:2016/12/1112:55
     *描述:    观察者
     */
    
    public interface Watcher {
        //更改通知
        public void update(String string);
    }

    这里就很好理解了,我们定义了两个接口,观察者和被观察者,观察者发送消息,被观察者添加/取消/通知观察者,那我们来看具体实现

    3.被观察者实现

    /*
     *项目名: RxSample
     *包名:   com.liuguilin.rxsample.java
     *文件名:  ConcreteWatched
     *创建者:  LGL
     *创建时间:2016/12/11 13:00
     *描述:    被观察者
     */
    public class ConcreteWatched implements Watched{
    
        private List<Watcher>mList = new ArrayList<>();
        @Override
        public void addWatcher(Watcher watcher) {
            mList.add(watcher);
        }
        @Override
        public void removeWatcher(Watcher watcher) {
            mList.remove(watcher);
        }
        @Override
        public void notifyWatcher(String string) {
            for(Watcher watcher:mList){
                watcher.update(string);
            }
        }
    }

    4.观察者实现

    /*
     *项目名: RxSample
     *包名:   com.liuguilin.rxsample.java
     *文件名:  ConcreteWatcher
     *创建者:  LGL
     *创建时间:2016/12/1113:18
     *描述:    观察者
     */
    public class ConcreteWatcher implements Watcher{
        @Override
        public void update(String string) {
            System.out.print(string);
        }
    }

    这里,我们的具体实现,只是对数据做了一些处理,我们通过addWatcher添加观察者,当被观察者达到某种条件的时候,我们就通知观察者打印出来,这样就是一整套的逻辑了,好,那我们再来编写一个测试的类,看看实际的效果如何

    5.Test

    /*
     *项目名: RxSample
     *包名:   com.liuguilin.rxsample.java
     *文件名:  Test
     *创建者:  LGL
     *创建时间:2016/12/1113:02
     *描述:    测试类
     */
    public class Test {
        public static void main(String [] args) throws Exception{
            Watched  thief = new ConcreteWatched();
    
            Watcher police1  = new ConcreteWatcher();
            Watcher police2  = new ConcreteWatcher();
            Watcher police3  = new ConcreteWatcher();
    
            //订阅
            thief.addWatcher(police1);
            thief.addWatcher(police2);
            thief.addWatcher(police3);
    
            thief.notifyWatcher("Steal things \n");
        }
    }

    这里我实现了一个被观察者,又实现了三个观察者,当被观察者开始Steal things的时候,观察者就会打印,那我们Run main一下看看是否真的打印出来了

    这里写图片描述

    写这个例子只是让大家明白一下,这个观察者模式的概念,这对我们学习Rx的思想模型有很大的帮助,大家可以继续回味一下,或者下载Sample去查看一下

    Sample下载:系列最后一篇提供

    本篇作为第一篇,还是一点点的把思路带进去吧,看下这样的效果好不好,并且我的博客排版更换了一下风格,这样的样式大家喜欢吗?

    别说了,加群吧:555974449

    展开全文
  • 思想通 万事通

    千次阅读 热门讨论 2016-09-20 11:04:12
    思想通万事通,我觉得这是肯定的,思想是根,是行动的根,而行动又是你成功之树的根,所以思想是本,在这位智者的指引之下我发现我的思想开始慢慢改变我开始看见以前我看不见的虽说还有很多我依旧看不见,你可能无法...
  • 函数式编程思想概论

    千次阅读 2019-06-26 10:57:16
    函数式编程思想概论前言函数λ 演算λ项绑定变量和自由变量约简α 变换β 约简η 变换纯函数、副作用和引用透明性函数式编程与并发编程总结 原文地址 前言 在讨论函数式编程(Functional Programming)的具体内容...
  • 我的管理思想

    万次阅读 2013-11-17 11:36:13
    我的管理思想一、中国古代哲学对我管理思想的影响我也是受中国传统哲学思想学习长大。中国主流儒学讲究仁(孔子)义(孟子),四大名著《三国》突出仁(火烧新野)义(千里走单骑/火烧连营)、《水浒》突出仁(替...
  • 算法的领悟():分治思想

    千次阅读 2010-05-20 09:36:00
    5分治思想与递归()我们可以下一个不太严谨的论断,枚举是算法思想的最本源,任何算法问题不是可以认为是寻找解吗?对枚举进行精化和特化的回溯和分支限界思想,在操作表现为对解空间的搜索;分治思想(Divide ...
  • 经常看到有初学者的提问,本人零基础,想学FPGA,求有经验的人说说,我应该哪入手,应该看什么教程,应该用什么学习板和开发板,看什么书等,希望有经验的好心人能够给我一些引导。FPGA到底怎么学呢?如果想速成,...
  • 软件工程思想

    2007-06-06 14:33:56
    “大学教育以填鸭的方式给学生灌输技能,却忽视传授思想和方法;学生们埋头学习,不知学以致用,离开学校后对企业、产品、人生一片茫然;企业以混乱甚至错误的方式开发产品,重复着前人的失败而不是成功。这就是产生...
  • 希腊几何学的社会文化根源

    千次阅读 2010-01-08 19:50:00
    【内容提要】本文古希腊独特的社会文化形态作为切入点,探索求解了数学思想著名的“克莱因问题”,从而破解了希腊证明几何学的成因之谜。古希腊社会在氏族社会向民族社会转轨变形的过程中,爆发了一场绵延数...
  • Kafka设计思想的脉络整理

    万次阅读 2018-03-18 00:44:30
    Kafka是一个被精心设计的东西,我只能这样。我这里所谓的精心不是它很完备的实现了某种规范,像个学生那般完成了某个作业,比如JMS,恰恰相反,Kafka突破了类似JMS这种规范性的束缚,它是卓越的,乃yet another ...
  • 网上解决的问题这里就不再赘述,下面的是我碰到的问题,同时也叙述了“TOKEN验证失败”的根源问题。背景 之前,我的公众号一直正常工作,但由于需要更换服务器,就把网站做了迁移,数据也迁移的,但是在修改服务器...
  • 分布式系统常用思想和技术

    千次阅读 2017-11-16 20:05:33
    由于服务和数据分布在不同的机器,每次交互需要跨机器运行,这带来如下几个问题: 1. 网络延迟:性能、超时 同机房的网络IO还是比较块的,但是跨机房,尤其是跨IDC,网络IO就成为不可忽视的性能瓶颈了。并且,...
  • 浅析android系统设计中的回调思想

    千次阅读 2015-05-22 20:06:51
    比如activity的生命周期,fragment的生命周期,皆是回调函数实现的,android中的事件处理机制其一就是回调,线程方面的异步任务、loader、hanlder等是基于回调的,等等。  可见android的其一根本思想就是回调,...
  • 本文专利客体“产品”向“方法”拓展的历史过程出发,揭示专利法区分抽象思想和具体技术的传统标准——“物质状态改变”。本文认为,专利法区分抽象思想与具体技术的传统标准并不否定计算机程序算法的客体属性。...
  • 麦卡锡称这种语言为Lisp, 意为List Processing, 因为他的主要思想之一是用一种简单的数据结构表(list)来代表代码和数据. 值得注意的是,麦卡锡所作的发现,不仅是计算机史划时代的大事, 而且是一种在我们这个时代...
  • 据哈佛大学研究:一个人没出息一定有以下这九大根源。1.犹豫不决:比鲁莽更糟糕的是犹豫不决。像墙头草一样摇摆不定的人,无论其他方面多么强大,在生命的竞赛中总是容易被那些坚定地人挤到一边。雷厉风行难免会犯错...
  • 目的 廓清欧拉变分法基本方程不变性思想及其产生的历史根源。方法 历史分析和文献考证。结果 欧拉基本方程不变性思想与18世纪分析学研究对象变革的大背景密切相关。随着分析学逐渐脱离几何传统,抽象的公式或作为...
  • .NET面临信任危机,根源在于目标模糊

    万次阅读 热门讨论 2005-03-15 13:17:00
    根源在于目标模糊(2005年第4期《程序员》文章,有删节,全文请阅读杂志) Richard Grimes是全世界最有名的几个.NET技术专家和作家之一,他不仅撰写过大量的.NET文章和技术书籍,而且作为MSDN Magazine的专栏作者和...
  • 欧盟《通用数据保护条例》提案在律师和法律学者之间引起了广泛的辩论,并且在被遗忘权问题表达了许多意见。 为了分析提案第17条提供的新规则的相关性,本文考虑了被遗忘权的原始思想,该思想早已存在于欧美法律...
  • HOOK启思录---第二章 HOOK的根源

    千次阅读 2006-11-22 22:33:00
    相对于了解一个成熟的技术,还不如去了解其中的思想吧。我是这样认为的。这一章会暂时远离HOOK本身,我们会尝试着去思考这些技术出现的根源。很多人象我一样,OP的衰败中走向OO的繁荣。软件的复杂度也是突飞猛进。...
  • 我们并不是微服务的风格是新颖的或创新的,它的根源至少可以追溯到 Unix 的设计原则(还要往前)。 但我们认为,没有足够多的人考虑在应用程序中使用微服务架构风格,如果他们使用了,或许软件开发将会变得更容易...
  • 哲学有三个经典问题: 1、你是谁 2、你哪里来 3、要到哪里去 而我们科学任何新知识产生也有三点: 1、你是谁——数学模型 ...2、你哪里来——模型设计根源 3、要到哪里去——模型应应用场合
  • 禅宗思想的哲学分析

    千次阅读 2018-05-21 17:56:17
    1.真如法性: 如如不动,不动如如,能如适一切种缘境(不论极恶还是极好的缘聚能如适,只要不妄心起相动念,一切是不同风味的体验而已!)。 “能如适”极恶缘者:将头临白刃,还如断春风。烈火燃东海,五岳相...
  • 思想的芦苇——把过程改进注入人文服务的思想以提升其价值龚云卿软脑软件(北京)有限公司 过程咨询部部长 兼 SEPG经理PDF文档下载法国思想家帕斯卡尔[[1]]过:“思想形成人的伟大。人只不过是一根芦苇,是自然界...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,943
精华内容 9,977
关键字:

从思想根源上说都是