-- Why mastering higher-order techniques and Functional programming is a good idea if you are writing programs in Javascript
(Javascriptでプログラムを書くなら高階プログラミングや 関数プログラミングをマスターするといいというワケ)
Preparing this talk was really hard for many reasons, chief amongst them being: We Perl/Javascript programmers are sort of divided into 2 groups - the first being those who are already big-time experts in higher-order techniques, and the second being those who are really not into these language features, or may not even know them.
(このトークの準備はいろいろ大変でした。 なにしろこの辺の話はみなさんすごくよく知っているか、 まったく知らないかのどちらかですから)
It was pretty hard to come up with content that might be interesting (and new) to both camps.
(全員に楽しんでもらえるコンテンツってなかなか思いつかなかったんです)
(この話のモトネタ)
(系譜)
Most people are familiar with this book, but just in case:
(ほとんどの方はご存知だと思いますが念のため)
consing
cake to your mouth, recursively defined cookie recipes,
and even a banquet (after the meta-circular interpreter
is completed). (ピーナツバターとジャムのサンドイッチとか、再帰定義されたクッキーのレシピとか、おいしいものがたくさん紹介されている)(なんでJavascriptの開発にLispの本が要るワケ?)
(私がLisp厨だからだと思ったでしょ?)
LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot.
(理解できたときのあの世界が広がる感じは、LISPを使わなくても一生の糧になるよ)
-- Eric S. Raymond
lambdas, or anonymous functions
is something that has influenced so many other languages (ラムダの概念にインスパイアされた言語も多い)(Javascriptで関数プログラミング)
Javascript is the Worlds's Most Misunderstood Programming Language
(Javascriptは世界でもっとも誤解されているプログラミング言語だ) -- Douglas Crockford
Douglas Crockford is responsible for the JSON idea. (json.org) (Douglas CrockfordというのはJSONの人)
He's also behind JSLINT (jslint.com), a lint program for Javascript (JSLINTの人でもある)
Crockford also wrote The Little Javascripter, which is a function-for-function rewrite of The Little Schemer in Javascript. (「The Little Schemer」をもとにした「The Little Javascripter」というのも書いている)
Effectively, the little Javascripter ends up implementing a scheme interpreter. (この本は最後にSchemaのインタプリタを実装)
Not a Javascript Interpreter, but a Scheme interpreter (JSのインタプリタではない)
(lambda (x) (* x x)) |
function (x) { return x * x; }
|
(define sqr (lambda (x) (* x x))) |
var sqr = function (x) { return x * x; };
function sqr(x) { return x * x; };
|
foo? |
isFoo() |
(cond ((hungry? me) (eat snack))
((thirsty? me) (drink a-drink))
(else
(hmm)))
|
isHungry(me) ?
eat(snack) :
isThirsty(me) ?
drink(aDrink) :
hmm(); // Nothing to do
|
Functional languages such a Lisp were initially used for AI research; and a lot of it was (then considered to be) about processing lists, and since in Lisp programs can be constructed out of lists, programs could write programs. (Lispのような関数型言語はもともとリスト処理の多いAIの研究に使われていた。Lispはプログラムもリストで書けるので、プログラムもプログラミングできる)
cons cell, which
in Javascript is just an array: (Little Javascripterのデータはもっぱら木構造。consも単なる配列になっている)
(add-all 1 2 3)In Javascript:
var runThis = ['addAll', [1, [2, [3, []]]]]; var action = car(runThis); var args = cdr(runThis);
And that's why the Little Javascripter has code like this: (だからこんなコードがあるわけです)
"What is (insertR new old listo) "Where new is yaki,old is tako and listo is: (yaki soba and tako and beer) |
(yaki soba and tako yaki and beer) |
(define insertR
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons old
(cons new (cdr lat))))
(else (cons (car lat)
(insertR new old
(cdr lat)))))))
|
function insertR(neo, old, lat) {
return isNull(lat) ? null :
isEq(car(lat), old) ?
cons(old,
cons(neo, cdr(lat))) :
cons(car(lat),
insertR(neo, old,
cdr(lat)));
}
|
Again, hardly optimal code for inserting elements into a list or any ordered collection - but to see how it all fits in, just going along with the book is great fun and enlightening at the same time. (配列に要素を挿入するという意味では最適なコードではないけれど、どんな風に置き換えていくのか読み進めていくだけでもおもしろいし、ためになる)
The Programming Languages Weblog lambda-the-ultimate.org, is named after Chapter 8 of The Little Schemer: (この名前はThe Little Schemerの第8章から)
Lets saw we need more functions like insertR: (さらに関数が必要になったとしましょう)
listo is (tako yaki and soba with beer) (insertL 'yaki 'soba listo) -> (tako yaki and yaki soba with beer) (insertR 'chilled 'with listo) -> (tako yaki and soba with chilled beer) (subst 'calpis 'beer listo) -> (tako yaki and soba with beer) (rember 'yaki listo) -> (tako and soba with beer)
Well, going back to the previous attempt to define insertR: (insertRの定義の仕方を見直します)
(define insertR
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons old
(cons new (cdr lat))))
(else (cons (car lat)
(insertR new old
(cdr lat)))))))
The highlighted texts indicate the only unique pieces in subroutines that are otherwise exactly the same. (強調されている部分はそのサブルーチンに特有の部分。あとはみんな同じです)
(define insertR
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons old
(cons new (cdr lat))))
(else (cons (car lat)
(insertR new old
(cdr lat)))))))
(define insertL
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons new lat))
(else (cons (car lat)
(insertL new old
(cdr lat)))))))
(define subst
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons old
(cdr lat)))
(else (cons (car lat)
(subst new old
(cdr lat)))))))
(define rember
(lambda (old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cdr lat))
(else (cons (car lat)
(rember old
(cdr lat)))))))
In this example, the redundancy does not really hurt very badly from a practical standpoint; however, in real-world situations, we very often see subroutines (or methods) whose content is mostly similar stuff. Too often, the redundancy can be huge enough to be considered a problem. (この例の場合は冗長化しても悪くはないですが、実際にはほとんど同じ内容のサブルーチンとかメソッドにしてしまうことが多いですね。冗長化すると嫌になるほど大きくなりすぎてしまいがちですから)
(究極のラムダ: 関数を返す関数)
The Functional Programming approach to a simple (toy) problem like this is pretty straight-forward: (こういうシンプルな問題の場合、関数プログラミング的なアプローチの方がストレート)
Instead of writingnsimilar functions, write a function that creates these functions for us. (同じような関数を何回も書くより、そういう関数をつくってくれる関数を書いた方がいい)
Imagine a function that returns a function: (関数を返す関数というのはこんなの)
| Scheme | Javascript | |
|---|---|---|
| Vanilla |
(define add1
(lambda (x)
(+ x 1)))
|
var add1 = function (x) {
return x + 1;
};
|
| (Still) Vanilla |
(define add2
(lambda (x)
(+ x 2)))
|
var add2 = function (x) {
return x + 2;
};
|
| Curry (!?) |
(define make-adder
(lambda (n)
(lambda (x)
(+ n x))))
|
var makeAdder = function (n) {
return function (x) {
return x + n;
};
};
|
The inner, highlighted functions are what are known as lexical closures. A feature that Scheme was famous for bringing to the Lisp world, and a feature that Javascript shares. (この強調されている関数はレキシカルクロージャといいます。この機能を最初にLisp界に紹介したのはSchemaですが、Javascriptにもあるんですね)
(究極のラムダ: 抽象化)
This function-returning function knows how
to build the n functions we are interested
in,
If we can pass this function-creating function a function
as an argument, we can tell it what flavor (of the n types)
(この関数を返す関数は必要な関数のつくりかたを知っていますので、この関数を引数にすればほかの関数にもどんな味にすればいいか伝えられるわけです)
| Scheme | Javascript | |
|---|---|---|
| The boring way (このやり方には飽きた) |
(define insertR
(lambda (new old lat)
(cond
((null? lat) '())
((eq? (car lat) old)
(cons old
(cons new (cdr lat))))
(else (cons (car lat)
(insertR new old
(cdr lat)))))))
|
function insertR(neo, old, lat) {
return isNull(lat) ? null :
isEq(car(lat), old) ?
cons(old,
cons(neo, cdr(lat))) :
cons(car(lat),
insertR(neo, old,
cdr(lat)));
}
|
| function maker (関数をつくる関数) |
(define insert-g
(lambda (seq)
(lambda (new old l)
(cond
((null? l) '())
((eq? (car l) old)
(seq new old (cdr l)))
(else
(cons (car l)
((insert-g seq) new old
(cdr l))))))))
|
function insertG(sisEq) {
return function (neo, old, l) {
return isNull(l) ? null :
isEq(car(l), old) ?
sisEq(neo, old, cdr(l)) :
cons(car(l),
insertG(sisEq)(neo, old, cdr(l)));
};
}
|
| insertR redefined insertRを再定義 |
(define insertR
(insert-g
(lambda (new old l)
(cons old
(cons new l)))))
|
var insertR = insertG(function (neo, old, l) {
return cons(old, cons(neo, l))
});
|
| insertL redefined insertLも再定義 |
(define insertL
(insert-g
(lambda (new old l)
(cons new
(cons old l)))))
|
var sisEqL = function (neo, old, l) {
return cons(neo, cons(old, l))
};
var insertL = insertG(sisEqL);
|
| subst redefined substを再定義 |
(define subst
(insert-g
(lambda (new old rest)
(cons new rest))))
|
var subst = insertG(function (neo, old, rest) {
return cons(neo, rest);
});
|
| rember redefined remberも再定義 |
(define rember
(insert-g
(lambda (new old rest)
rest)))
|
var rember = insertG(function (neo, old, rest) {
return rest;
});
|
これは説明しないよ
function Y(le) {
return function (f) {
return f(f);
}(function (f) {
return le(function (x) {
return f(f)(x);
});
});
}
var factorial = Y(function (fac) {
return function (n) {
return n <= 2 ? n : n * fac(n - 1);
};
});
var number120 = factorial(5);
(Transforming Programs with Programs) (プログラムでプログラムを変形する)
s/Perl/Javascript/and the explanations will still make as much sense. (そんなにおかしなことにはならないんです)
Higher Order Perl - プログラムでプログラムを変形する
Look at what MJD says about Perl and C: (PerlとCについてはこんなことを言ってます) (From H.O.P):
"We, as Perl programmers, have been writing C programs in Perl whether we meant to or not. This is a shame, because Perl is a much more expressive language than C. We could be doing a lot better, using Perl in ways undreamt of by C programmers, but we're not."
(Perlプログラマは長年PerlでCを書いてきましたが、 これは残念なこと。だってPerlの方が表現力があるんですもの、もっとうまいやり方があるんですよ)
"... Then you can stop writing C programs in Perl. I think that you will find it to be a nice change. Perl is much better at being Perl than it is at being a slow version of C. You will be surprised as what you can get done when you write Perl programs instead of C."
(Perlは遅いCじゃないんですから、 Cっぽく書かなくてもいいんです。 Perlらしく書いたら、ここまでできるのかって驚きますよ)
Here, you can really
s/Perl/Javascript/gand it still makes total sense. (この辺はPerlをJavascriptで置き換えてもまったく問題ないですよね)
s/Perl/Javascript/g
"We, as Javascript programmers, have been writing C programs in Javascript whether we meant to or not. This is a shame, because Javascript is a much more expressive language than C. We could be doing a lot better, using Javascript in ways undreamt of by C programmers, but we're not."
(Javascriptプログラマは長年JavascriptでCを書いてきましたが、 これは残念なこと。だってJavascriptの方が表現力があるんですもの、もっとうまいやり方があるんですよ)
"... Then you can stop writing C programs in Javascript. I think that you will find it to be a nice change. Javascript is much better at being Javascript than it is at being a slow version of C. You will be surprised as what you can get done when you write Javascript programs instead of C."
(Javascriptは遅いCじゃないんですから、 Cっぽく書かなくてもいいんです。 Javascriptらしく書いたら、ここまでできるのかって驚きますよ)
(余談というか告解)
(Perlプログラマとしての私)
As far as Perl was concerned, I'm totally guilty of this as well - for years I just used Perl as a better sed or awk. I mean it was, and that's all good, and I wasn't really building large programs as much as I was using Perl in it's one-liner strength and for use-once throw-away scripts - but discovering the cool uses of anonymous sub refs (I think it first picked up some while reading Effective Perl by Joseph N. Hall, Randal Schwartz) was a huge, huge help as I started writing proper programs.
(私は長年Perlをsedやawkのように使ってきました。 ワンライナーとか使い捨てのスクリプトばかりで大きなプログラムなんて書いていなかったんです。 まともなプログラムを書くようになったのはクロージャを使えるようになってからでした)
(Higher-order-PerlプログラマのためのHigher-Order-Javascript)
This is a collection of code snippets in Javascript (and annotations) that basially cover a familiar subset of the Perl programs described in MJD's book.
(基本的にMJDの本に書いてあったPerlのプログラムをJavascriptに移植したもの)
The Author shows the slight adaptations required in JS code to account for the Perl vs Javascript language differences.
(PerlとJSの違いを説明するためにちょっとだけ手直しが入っている)
| Perl | Javascript | |
|---|---|---|
| no block scope (ブロックスコープがない) |
{
my $very_own_var;
} |
(function () {
var myVeryOwn = ...;
})(); |
| and no 'do' either (doもない) |
my $result = dodo {
my $foo = something();
my $bar = something_else();
$foo + $bar;
} |
var result = (function () {
var foo = something();
var bar = something_else();
return foo + bar;
})(); |
(Perlの強み)
Here's a few instances where Perl's perlishness is a huge plus, forcing Sean to come up with not-so-pretty substitutes: Verbatim from HOJ:
(こういうのを見るといかにPerlらしく書けるのが便利かわかりますね)
In Perl: JavaScript Workaround:
unless($x) ... if(!x)...
=> JavaScript has no 'unless' or 'until'
$x = funcname $y; x = funcname(y);
=> You can't leave the parens off of a
function call; but note that
"throw" and "return" aren't
functions, so their parens are
optional.
dostuff() || return 123; if(! dostuff() ) return "Ugh";
dostuff() || die "Ugh"; if(! dostuff() ) throw "Ugh";
=> "throw" is a statement, and so
can't be a component of an
expression, like the operands
of "x || y" are.
Ditto for "return".
$x = "Your name is $name." x = "Your name is " + name + ".";
=> JavaScript doublequoted
string-literals don't have
variable interpolation.
But see the format() function I
write in section 1.3.
(リストと連鎖呼び出し)
And here's where JS's prototype-based properties produce 'flipped' code, which looks much better (for JS at least):
(JSもプロトタイプベースのプロパティを使うと見栄えがよくなります)
| Perl | Javascript |
|---|---|
join '/',
map $_*2,
split ':',
$thing
|
thing
.split( ':' )
.map( function(_){return $_*2} )
.join('/')
|
Without JavaScript's prototype-based extension properties, We could have to make do with the less pretty:
(そのまま書き換えるよりきれいでしょ?)
map( function(_){return $_*2},
thing
.split( ':' )
)
.join('/')
(Prototypeに至るまで)
Array is (Javascriptで配列をつくるオススメの方法は)var arr = []; // recommended; Nice and short var arr = new Array(); // The same, actually
arr.sort(); arr.splice();
Array.prototype.map = function(f) {
if(!f.apply) { var propname = f; f = function(_) { return _[propname] } }
var out = [];
for(var i = 0; i < this.length; i++) {
out.push( f( this[i], this, i) );
}
return out;
};
map method: (これで自動的にmapメソッドが追加されます)
[1,2,3,4].map( function (_) { return _ * _; } );
// [1, 4, 9, 16]
(Prototypeに至るまで)
Prototype, or Prototype.js, which is slightly Ruby inspired (both OO friendly and lambda-friendly) makes extensive use of this to provide really useful utilities that exist as methods that you can just call
(Prototype.jsはこのテクニックをふんだんに使って便利な機能を提供しています)
If you are using Prototype.js, then you've already got a wealth of higher-order utilities which can greatly reduce your source-code size if used properly.
(Prototype.jsを使っている人はもう高階ユーティリティの恩恵を受けているはず。 うまく使えばソースコードをうんと小さくできますよね)
(余談)
There's a lot of Functional techniques that we (err, Javascript developers?) all use, almost on a daily basis, even though we never stop to think, "hey, that's Functional":
(みなさんもよく関数プログラミングのテクニックを使っているんですよ)
sort() method's comparator is a functional object
(sortのときもシンプルな関数インタフェースを便利に使っています)(例: ソート)
var unsorted = [
{ name: "Bilbo", age: 111 },
{ name: "Frodo", age: 20 },
{ name: "Sam", age: 22}
];
unsorted.sort(function (aHobbit, anotherHobbit) {
return aHobbit.age == anotherHobbit.age ? 0 :
aHobbit.age > anotherHobbit.age ? 1 : -1;
});
In Java:
Collection<Hobbit> hobbits = ...;
Comparator comp = new Comparator() {
public int compare (o1, o2) {
Hobbit aHobbit = (Hobbit) o1;
Hobbit anotherHobbit = (Hobbit) o2;
return aHobbit.age == anotherHobbit.age ? 0 :
aHobbit.age > anotherHobbit.age ? 1 : -1;
}
};
Collections.sort(hobbits, comp);
(例: イベントハンドラ)
loginButton.addEventListener( 'click', function () {
// stuff
});
(例: 関数もオブジェクトなのでOOにした方がいい)
A.Nice.Widget = function (arg1, arg2) {
/* constructor */
};
// Methods
A.Nice.Widget.prototype = {
toString: function () {
return "[String Representation]";
},
// lot's of methods defined here
renderTrickyBit: function () {
if (ENV.isIE) {
// $MS stuff
} else if (ENV.isGecko) {
// FF, Mozilla stuff
} else if (ENV.isOpera) {
// Opera stuff
} else {
// Give up, fallback to
// not-so rich rendering
}
},
// and more methods
}; |
A.Nice.Widget = function (arg1, arg2) {
/* constructor */
};
// Methods
A.Nice.Widget.prototype = {
toString: function () {
return "[String Representation]";
},
// lot's of methods defined here
renderTrickyBit:
ENV.isIE ? function () {
// $MS stuff
} :
ENV.isGecko ? function () {
// FF/Mozilla stuff
} :
ENV.isOpera ? function () {
// Opera stuff
} :
// The fallback...
function () {
// boring...
},
// and more methods
};
|
(まとめ)