神奇的阿基米德螺线

2014年5月23日 19:06

今天在读数学史,正巧读到阿基米德螺线,于是写了一段js代码,生成螺线。

See the Pen Spiral by gangtao (@gangtao) on CodePen.

更多的有趣内容请参考这篇文章

代码如下(需要jquery和d3):

HTML

<div id="chart"></div>

 

CSS

body {
background-color:#000000;
}

 

JS

function drawCircile(pc, r, container) {
    var circle = container.append("circle").attr("cx", pc.x).attr("cy", pc.y).attr("r", r)
        .attr("stroke", "#fa6900").attr("stroke-width", 1).attr("fill", "none");
    return circle;

}

function drawLine(p1, p2, c, isDash) {
    var line = c.append("line").attr("x1", p1.x).attr("y1", p1.y)
        .attr("x2", p2.x).attr("y2", p2.y)
        .style("stroke", "#ccc").style("stroke-width", 1);
    if (isDash) {
        line.style("stroke-dasharray", "5,5");
    }
    return line;
}

$(function () {
    var center = {x: 300, y :300};
    var l = 250;
    var a = 0, b = 0, p0, p1, line;
    var v = 0.3; //直线移动速度
    var f = 360; //转动速度 

    var root = d3.select("#chart").append("svg").append("g");
    drawCircile(center,3,root)

    var timer = setInterval(function(){
        a = a + Math.PI / f;
        b = b + v;
        p0 = {x:center.x+ l * Math.sin(a), y:center.y - l * Math.cos(a) };
        p1 = {x:center.x + b * Math.sin(a), y:center.y - b * Math.cos(a) };
        drawCircile(p1,1,root)

        if( b > l ) {
            clearInterval(timer);
        } else {
            if(!line) {
                line =  drawLine(center, p0, root);    
            } else {
                line.transition().duration(10).attr("x2",p0.x).attr("y2",p0.y);
            } 
        }
    },10);
});

改变v和f的值分别改变点在直线上的移动过速度和直线转动的角速度,会得到不同的结果。

我拿给我还在读一年级的女儿看,她很是入迷,不断要求我修改参数,结果得到许多有趣的图形,大家可以通过这个链接自己试试看。

f=2.5 海星

f=3 雪花

f=1.414

f = 0.618

f=8.8

f = 12.12

更多的结果,大家可以自己去探索。

感叹于数学的神奇和美丽,感叹于小孩子的探索精神, 感叹于计算机和编程使得数学的探索变得简单和有趣!

 

探索Javascript异步编程

2014年5月21日 17:52

笔者在之前的一片博客中简单的讨论了Python和Javascript的异同,其实作为一种编程语言Javascript的异步编程是一个非常值得讨论的有趣话题。

JavaScript 异步编程简介

回调函数和异步执行

所谓的异步指的是函数的调用并不直接返回执行的结果,而往往是通过回调函数异步的执行。

我们先看看回调函数是什么:

var fn = function(callback) {
    // do something here
    ...
    callback.apply(this, para);
};

var mycallback = function(parameter) {
    // do someting in customer callback
};

// call the fn with callback as parameter
fn(mycallback);

 

回调函数,其实就是调用用户提供的函数,该函数往往是以参数的形式提供的。回调函数并不一定是异步执行的。比如上述的例子中,回调函数是被同步执行的。大部分语言都支持回调,C++可用通过函数指针或者回调对象,Java一般也是使用回调对象。

在Javascript中有很多通过回调函数来执行的异步调用,例如setTimeout()或者setInterval()。

setTimeout(function(){
    console.log("this will be exectued after 1 second!");
},1000);

 

在以上的例子中,setTimeout直接返回,匿名函数会在1000毫秒(不一定能保证是1000毫秒)后异步触发并执行,完成打印控制台的操作。也就是说在异步操作的情境下,函数直接返回,把控制权交给回调函数,回调函数会在以后的某一个时间片被调度执行。那么为什么需要异步呢?为什么不能直接在当前函数中完成操作呢?这就需要了解Javascript的线程模型了。

Javascript线程模型和事件驱动

Javascript最初是被设计成在浏览器中辅助提供HTML的交互功能。在浏览器中都包含一个Javascript引擎,Javscript程序就运行在这个引擎之中,并且只有一个线程。单线程能都带来很多优点,程序员们可以很开心的不用去考虑诸如资源同步,死锁等多线程阻塞式编程所需要面对的恼人的问题。但是很多人会问,既然Javascript是单线程的,那它又如何能够异步的执行呢?

这就需要了解到Javascript在浏览器中的事件驱动(event driven)机制。事件驱动一般通过事件循环(event loop)和事件队列(event queue)来实现的。假定浏览器中有一个专门用于事件调度的实例(该实例可以是一个线程,我们可以称之为事件分发线程event dispatch thread),该实例的工作就是一个不结束的循环,从事件队列中取出事件,处理所有很事件关联的回调函数(event handler)。注意回调函数是在Javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。

Event Loop Code:

while(true) {
 var event = eventQueue.pop();
 if(event && event.handler) {
     event.handler.execute(); // execute the callback in Javascript thread
 } else {
     sleep(); //sleep some time to release the CPU do other stuff
 }
}

 

通过事件驱动机制,我们可以想象Javascript的编程模型就是响应一系列的事件,执行对应的回调函数。很多UI框架都采用这样的模型(例如Java Swing)。

那为什要异步呢,同步不是很好么?

异步的主要目的是处理非阻塞,在和HTML交互的过程中,会需要一些IO操作(典型的就是Ajax请求,脚本文件加载),如果这些操作是同步的,就会阻塞其它操作,用户的体验就是页面失去了响应。

综上所述Javascript通过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的IO操作。

Javascript异步编程带来的挑战

Javascript的单线程模型有很多好处,但同时也带来了很多挑战。

代码可读性

想象一下,如果某个操作需要经过多个非阻塞的IO操作,每一个结果都是通过回调,程序有可能会看上去像这个样子。

operation1(function(err, result) {
    operation2(function(err, result) {
        operation3(function(err, result) {
            operation4(function(err, result) {
                operation5(function(err, result) {
                    // do something useful
                })
            })
        })
    })
})

 

我们称之为意大利面条式(spaghetti)的代码。这样的代码很难维护。这样的情况更多的会发生在server side的情况下。

流程控制

异步带来的另一个问题是流程控制,举个例子,我要访问三个网站的内容,当三个网站的内容都得到后,合并处理,然后发给后台。代码可以这样写:

var urls = ['url1','url2','url3'];
var result = [];

for (var i = 0, len = urls.length(); i < len; i++ ) {
    $.ajax({
        url: urls[i],
        context: document.body,
        success: function(){
          //do something on success
          result.push("one of the request done successfully");
          if (result.length === urls.length()) {
              //do something when all the request is completed successfully
          }
        }});
}

 

上述代码通过检查result的长度的方式来决定是否所有的请求都处理完成,这是一个很丑陋方法,也很不可靠。

异常和错误处理

通过上一个例子,我们还可以看出,为了使程序更健壮,我们还需要加入异常处理。 在异步的方式下,异常处理分布在不同的回调函数中,我们无法在调用的时候通过try...catch的方式来处理异常, 所以很难做到有效,清楚。

更好的Javascript异步编程方式

“这是最好的时代,也是最糟糕的时代”

为了解决Javascript异步编程带来的问题,很多的开发者做出了不同程度的努力,提供了很多不同的解决方案。然而面对如此众多的方案应该如何选择呢?我们这就来看看都有哪些可供选择的方案吧。

Promise

Promise 对象曾经以多种形式存在于很多语言中。这个词最先由C++工程师用在Xanadu 项目中,Xanadu 项目是Web 应用项目的先驱。随后Promise 被用在E编程语言中,这又激发了Python 开发人员的灵感,将它实现成了Twisted 框架的Deferred 对象。

2007 年,Promise 赶上了JavaScript 大潮,那时Dojo 框架刚从Twisted框架汲取灵感,新增了一个叫做dojo.Deferred 的对象。也就在那个时候,相对成熟的Dojo 框架与初出茅庐的jQuery 框架激烈地争夺着人气和名望。2009 年,Kris Zyp 有感于dojo.Deferred 的影响力提出了CommonJS 之Promises/A 规范。同年,Node.js 首次亮相。

在编程的概念中,future,promise,和delay表示同一个概念。Promise翻译成中文是“承诺”,也就是说给你一个东西,我保证未来能够做到,但现在什么都没有。它用来表示异步操作返回的一个对象,该对象是用来获取未来的执行结果的一个代理,初始值不确定。许多语言都有对Promise的支持。

Promise的核心是它的then方法,我们可以使用这个方法从异步操作中得到返回值,或者是异常。then有两个可选参数(有的实现是三个),分别处理成功和失败的情景。

var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)

 

异步调用doSomethingAync返回一个Promise对象promise,调用promise的then方法来处理成功和失败。这看上去似乎并没有很大的改进。仍然需要回调。但是和以前的区别在于,首先异步操作有了返回值,虽然该值只是一个对未来的承诺;其次通过使用then,程序员可以有效的控制流程异常处理,决定如何使用这个来自未来的值。

对于嵌套的异步操作,有了Promise的支持,可以写成这样的链式操作:

operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});

 

Promise提供更便捷的流程控制,例如Promise.all()可以解决需要并发的执行若干个异步操作,等所有操作完成后进行处理。

var p1 = async1();
var p2 = async2();
var p3 = async3();
Promise.all([p1,p2,p3]).then(function(){
    // do something when all three asychronized operation finished
});

 

对于异常处理,

doA()
  .then(doB)
  .then(null,function(error){
      // error handling here
  })

 

如果doA失败,它的Promise会被拒绝,处理链上的下一个onRejected会被调用,在这个例子中就是匿名函数function(error){}。比起原始的回调方式,不需要在每一步都对异常进行处理。这生了不少事。

以上只是对于Promise概念的简单陈述,Promise拥有许多不同规范建议(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和开源实现。大家可以参考一下的这些链接。


如果你有选择困难综合症,面对这么多的开源库不知道如何决断,先不要急,这还只是一部分,还有一些库没有或者不完全采用Promise的概念

Non-Promise

下面列出了其它的一些开源的库,也可以帮助解决Javascript中异步编程所遇到的诸多问题,它们的解决方案各不相同,我这里就不一一介绍了。大家有兴趣可以去看看或者试用一下。

Non-3rd Party

其实,为了解决Javascript异步编程带来的问题,不一定非要使用Promise或者其它的开源库,这些库提供了很好的模式,但是你也可以通过有针对性的设计来解决。

比如,对于层层回调的模式,可以利用消息机制来改写,假定你的系统中已经实现了消息机制,你的code可以写成这样:

eventbus.on("init", function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});

eventbus.on("ACompleted", function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});

eventbus.on("BCompleted", function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});

eventbus.on("CCompleted", function(){
    // do something when all operation completed
});

 

这样我们就把嵌套的异步调用,改写成了顺序执行的事件处理。

更多的方式,请大家参考这篇文章,它提出了解决异步的五种模式:回调、观察者模式(事件)、消息、Promise和有限状态机(FSM)。

下一代Javscript对异步编程的增强

ECMAScript6

下一代的Javascript标准Harmony,也就是ECMAScript6正在酝酿中,它提出了许多新的语言特性,比如箭头函数、类(Class)、生成器(Generator)、Promise等等。其中Generator和Promise都可以被用于对异步调用的增强。

Nodejs的开发版V0.11已经可以支持ES6的一些新的特性,使用node --harmony命令来运行对ES6的支持。

co、Thunk、Koa

koa是由Express原班人马(主要是TJ)打造,希望提供一个更精简健壮的nodejs框架。koa依赖ES6中的Generator等新特性,所以必须运行在相应的Nodejs版本上。

利用Generator、coThunk,可以在Koa中有效的解决Javascript异步调用的各种问题。

co是一个异步流程简化的工具,它利用Generator把一层层嵌套的调用变成同步的写法。

var co = require('co');
var fs = require('fs');

var stat = function(path) {
  return function(cb){
    fs.stat(path,cb);
  }
};

var readFile = function(filename) {
  return function(cb){
    fs.readFile(filename,cb);
  }
};

co(function *() {
  var stat = yield stat('./README.md');
  var content = yield readFile('./README.md');
})();

 

通过co可以把异步的fs.readFile当成同步一样调用,只需要把异步函数fs.readFile用闭包的方式封装。

利用Thunk可以进一步简化为如下的code, 这里Thunk的作用就是用闭包封装异步函数,返回一个生成函数的函数,供生成器来调用。

var thunkify = require('thunkify');
var co = require('co');
var fs = require('fs');

var stat = thunkify(fs.stat);
var readFile = thunkify(fs.readFile);

co(function *() {
  var stat = yield stat('./README.md');
  var content = yield readFile('./README.md');
})();

 

利用co可以串行或者并行的执行异步调用。

串行

co(function *() {
  var a = yield request(a);
  var b = yield request(b);
})();

 

并行

co(function *() {
 var res = yield [request(a), request(b)];
})();

 

更多详细的内容,大家可以参考这两篇文章12

总结

异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。

同时,随着ES6的定义,Javascript的语法变得越来越丰富,更多的功能带来了很多便利,然而原本简洁,单一目的的Javascript变得复杂,也要承担更多的任务。Javascript何去何从,让我们拭目以待。

 

数据可视化中的视觉属性

2014年4月09日 16:10

Stephan Few 是数据可视化领域里面数一数二的专家,他的几本书《Show Me The Numbers》,《Information Dashboard Design》和《Now You See It》都是非常优秀的关于数据可视化的书。

Stephan Few 和 Tableau 的合作非常紧密,在 Tableau 的数据可视化的设计中,处处可见 Stephan Few 的思想的影子。Stephan Few 在他的博客中毫不掩饰的提到 Tableau 是他唯一欣赏和敬重的公司。

以下是我阅读《Now You See It》的读书笔记:

在书中提到了视觉属性在可视化中的作用,视觉属性包含了以下分类:

 

  • 形式

    • 长,宽, 方向, 大小, 形状, 弯曲度, 围绕, 模糊

  • 颜色

    • 色调, 强度

  • 空间位置

    • 2-D 位置, 空间分组

  • 动画

    • 方向

每一种属性在表达数据时的表现力度是不一样的,大家可以通过这个图(Design By Stephane Few)来体会一下,点这里看动态图(Implemented By JavaScript + D3.js)

把鼠标移动到你关注的视觉属性上看效果。
 

 

用动态可视化来讲故事

2014年4月07日 22:48

在科学松鼠会的网站上有一篇漫画,来自imgur.com,讲述了 科学理论是如何建立的

我用到d3.js把这个静态的图改成了动态效果,见这里。 代码可见github

注:这个其实主要就是奥卡姆剃刀原理,“若无必要,勿增实体”。

附原图 :

前些日子看了可汗学院的这个关于诚实人和说谎者的脑筋急转弯问题,我觉得如果能用程序来模拟,那一定很有趣。

这个题目是这样的,有两扇门,一扇通往天堂,一扇通往地狱,你要做出选择打开那扇门。门口各有一个人,他们都知道门后面的情况,其中一个总是说实话,而另一个总是撒谎。你可以问他们问题。要怎样问问题,才能做出正确的选择?

在揭开答案之前,我们先看看我是如何用JavaScript来模拟这个问题的。

function Game() {
  var destinations = ["Heaven", "Hell"];
  var gates = {}, persons = {};

  function Gate() {
    var val = undefined;
    return {
      value: function(_) {
        if (!arguments.length) {
          return val;
        }
        val = _;
      },
      open: function() {
        if (val === destinations[0]) {
          console.log("Happy Ending!");
        } else {
          console.log("You Die!");
        }
      }
    }
  }

  function Behavior(isLier) {
    var tellTruth = function(info) {
      return info;
    };
    var tellLie = function(info) {
      var index = destinations.indexOf(info);
      if (index === 0) {
        return destinations[1];
      } else {
        return destinations[0];
      }
    };
    return isLier ? tellLie : tellTruth;
  }

  function Person(gate, behavior) {
    return {
      ask: function() {
        return behavior.call(this, gate.value());
      }
    }
  }

  function Init() {
    gates.A = Gate();
    gates.B = Gate();

    if (Math.random() > 0.5) {
      gates.A.value(destinations[0]);
      gates.B.value(destinations[1]);
    } else {
      gates.A.value(destinations[1]);
      gates.B.value(destinations[0]);
    }

    if (Math.random() > 0.5) {
      persons.A = Person(gates.A, Behavior(true));
      persons.B = Person(gates.B, Behavior(false));
    } else {
      persons.A = Person(gates.A, Behavior(false));
      persons.B = Person(gates.B, Behavior(true));
    }
  }

  Init();

  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    }
  }
}

 

程序主要有以下方法:

  • Gate

    Gate方法负责创建们,每扇门提供两个方法, value方法负责读写们的内容,open方法检查开门后的结果。如果是天堂,则打印“Happy Ending”, 游戏成功;如果是地狱则打印“You Die”,游戏失败。

  • Behavior

    Behavior是对说谎和说实话这两种行为的抽象。返回两个不同的方法,说谎的行为总是返回和原本信息不一致的信息,而说实话则返回原本的信息。

  • Person

    Person方法负责创建人,并关联人所在的门,指定人的行为。

  • Init

    Init方法随机初始化门和人

  • Game

    Game方法是最外层的闭包,他暴露出游戏者可以调用的两个方法,问问题和开门。因为Person和Gate都在闭包里所以游戏者无法获得任何相关的信息,只能通过ask方法来问问题。

调用以下方法来运行游戏:

var g = Game();
console.log(g.ask("A"));
console.log(g.ask("B"));

 

因为A和B有一个人说谎,所以条用的结果是他们都说自己后面的门是“天堂”,或者都说是“地狱”。

于是无论打开A还是B,游戏者都只有50%的概率获胜。

好了,现在是揭晓答案的时候了,这道题,游戏者需要这样问,“如果问另外一个人你后面的门后是什么,他会怎么说?”会有两种情况:

  • 问诚实人说谎者怎么回答他后面的门是什么,因为说谎者会说谎话,而诚实的人会原封不动的返回这个回答,所以提问者会得到错误的答案

  • 问说谎者诚实人怎么回答他后面的门是什么,因为说谎者会说谎话,他会把诚实人的正确答案变成谎话回答,所以答案还是错误的。

那么我们要怎样修改我们的程序呢?

其实改动很简单,首先要增强Person方法

function Person(gate, behavior) {
    return {
      ask: function(aGate) {
        if(!aGate) {
          return behavior.call(this, gate.value());
        } else {
          return behavior.call(this, aGate.value());
        }
      },
      askBy: function(accordingTo) {
        return behavior.call(this, accordingTo.ask(gate));
      }
    }
  }

 

在Person方法中增强ask方法,暴露门的信息,也就是说每个人不但知道自己的门后的东西是什么,也能回答另一扇门后是什么。所以通过参数传递门的引用。这里我们可以看出,在前一版中假定人和门之间的耦合关系是不成立的,其实完全应该可以帮人和门解耦。

另外增加了askBy方法,这个方法会去问另一个人,我给你的门后面的信息,然后用自己的行为返回出去。

除了扩展Person,我们还要扩展Game返回的对象,增加新的提问方式:

  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    },
    askBy : function(personName, byName) {
      return persons[personName].askBy(persons[byName]);
    }
  }

 

这样,新暴露的askBy方法就可以正确的找到错误的门了(听上去很绕口)

增强后的代码如下:

function Game() {
  var destinations = ["Heaven", "Hell"];
  var gates = {}, persons = {};

  function Gate() {
    var val = undefined;
    return {
      value: function(_) {
        if (!arguments.length) {
          return val;
        }
        val = _;
      },
      open: function() {
        if (val === destinations[0]) {
          console.log("Happy Ending!");
        } else {
          console.log("You Die!");
        }
      }
    }
  }

  function Behavior(isLier) {
    var tellTruth = function(info) {
      return info;
    };
    var tellLie = function(info) {
      var index = destinations.indexOf(info);
      if (index === 0) {
        return destinations[1];
      } else {
        return destinations[0];
      }
    };
    return isLier ? tellLie : tellTruth;
  }

  function Person(gate, behavior) {
    return {
      ask: function(aGate) {
        if(!aGate) {
          return behavior.call(this, gate.value());
        } else {
          return behavior.call(this, aGate.value());
        }
      },
      askBy: function(accordingTo) {
        return behavior.call(this, accordingTo.ask(gate));
      }
    }
  }

  function Init() {
    gates.A = Gate();
    gates.B = Gate();

    if (Math.random() > 0.5) {
      gates.A.value(destinations[0]);
      gates.B.value(destinations[1]);
    } else {
      gates.A.value(destinations[1]);
      gates.B.value(destinations[0]);
    }

    if (Math.random() > 0.5) {
      persons.A = Person(gates.A, Behavior(true));
      persons.B = Person(gates.B, Behavior(false));
    } else {
      persons.A = Person(gates.A, Behavior(false));
      persons.B = Person(gates.B, Behavior(true));
    }
  }

  Init();

  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    },
    askBy : function(personName, byName) {
      return persons[personName].askBy(persons[byName]);
    }
  }
}

 

调用以下代码来问新的问题:

var g = Game();
console.log(g.askBy("A","B"));
console.log(g.askBy("B","A"));

 

游戏者只要打开和回答不一致的门就可以马上升入天堂!

通过这个例子,我们发现对Behavior的抽象对进一步问题的扩展起到了很大的帮助。这也展现了基于Function编程的灵活性,不难想象,如果我们把说谎者和诚实人按照面向对象的方式来构造,而不是把说谎还是说真话作为行为注入到一般的人上,对新的问题的扩展将会比较困难。