CoffeeScript の最初の一杯: 第 2 回 実践的な例から CoffeeScript 言語について学ぶ

この連載では、JavaScript をベースに作成された人気の高い CoffeeScript プログラミング言語について詳しく探ります。第 1 回では、このプログラミング言語が開発者にもたらすメリットを説明し、CoffeeScript コンパイラーをセットアップして、そのコンパイラーを使ってブラウザーやサーバーですぐに実行できる状態のコードを作成しました。第 2 回となるこの記事では、CoffeeScript 言語についてさらに詳しく探ります。今回は CoffeeScript を使用して、Project Euler から出題されているいくつかのプログラミング問題を数学的な趣向を加えて解きます。サンプルのソース・コードも用意されています。

Michael Galpin, Software Engineer, Google

Michael Galpin's photoMichael Galpin は、Google のソフトウェア・エンジニアです。彼は、『Android in iPractice』の共著者であり、developerWork にも頻繁に寄稿しています。彼が次に取り組もうとしている内容をこっそり覗くには、彼のブログを調べるか、Twitter の @michaelg または Google の +Michael Galpin で彼をフォローしてください。



2012年 3月 08日

はじめに

この連載では、JavaScript をベースに作成された新しい CoffeeScript プログラミング言語について探ります。簡潔な構文を提供する CoffeeScript は有効な JavaScript にコンパイルされるため、その JavaScript を Web ブラウザーで実行することも、Node.js などのサーバー・アプリケーション用の技術で使用することもできます。第 1 回では、CoffeeScript コンパイラーをセットアップして、ブラウザーでもサーバーでもすぐに実行できる状態のコードを作成する方法を学びました。

今回は、Project Euler から出題されているプログラミング問題をいくつか解くことで CoffeeScript 言語について探っていきます (Project Euler についての詳細は、「参考文献」を参照してください)。これらのプログラミング問題を解くためのサンプル・コードのなかで、CoffeeScript の関数、範囲指定、内包、ブロック文、配列、そしてオブジェクト指向の側面を使用する方法を具体的に説明します。

この記事で説明するソース・コードはここからダウンロードしてください。


関数、範囲指定、内包

まず始めに取り掛かる Project Euler の問題は、Problem 6 です (「参考文献」を参照)。この問題は、自然数の最初の 100 個について、まずはそれぞれの二乗の和を求め、次に同じ 100 個の自然数の和を二乗した値を求め、最後にこの 2 つの結果の差を答えるというものです。この問題には、CoffeeScript の REPL (Read-Evaluate-Print-Loop) を使用します。リスト 1 に、コードと REPL セッションによる出力を記載します。

リスト 1. REPL を使用して Problem 6 を解く
coffee> square = (x) -> x*x
[Function]
coffee> sum = (nums) -> nums.reduce (a,b) -> a+b
[Function]
coffee> diff = (nums) -> (square sum nums) - (sum nums.map square)
[Function]
coffee> diff [1..100]
25164150

以下に、このコードの内容を詳しく説明します。

  1. まず、REPL で関数を定義します (第 1 回で説明したように、CoffeeScript は JavaScript が持つ関数型プログラミングの特性を取り入れ、簡潔な関数型プログラミングを行う妨げとなる、JavaScript の C 言語風の構文の大部分は排除しています)。

    リスト 1 のサンプル・コードでは square という名前の関数を定義しており、この関数は引数を 1 つ取り、その引数を自分自身と掛け合わせた積 (つまり、二乗した値) を返すものとして記述されています。すると REPL が、関数が定義されたことを通知します。

  2. sum という名前の別の関数を定義します。この関数が取る唯一の引数は配列です。sum は、その配列で reduce メソッドを呼び出します。

    reduce メソッドは CoffeeScript で追加されたものではなく、JavaScript のれっきとした一部であり (JavaScript 1.8 で追加されました)、Python での reduce 関数、あるいは Haskell や Scala での fold 関数と似ています。reduce メソッドは引数として関数を取り、配列の要素に対して先頭要素から順に関数を適用し、返された結果と次の要素に対してまた関数を適用するという処理を繰り返すことで畳み込み演算を行います。CoffeeScript の簡潔な関数構文では、reduce を簡単に使用できるようになっています。このサンプル・コードの場合、reduce に渡す関数は (a,b) -> a + b と指定されています。これは 2 つの値を取ってそれを加算する関数なので、配列のすべての要素が加算されることになります。

  3. diff という名前の関数を作成します。この関数は数値の配列を引数に取り、2 つの部分式を計算して、それらの結果同士で減算を行います。最初の部分式は配列を sum 関数に渡し、その結果を取得して square 関数に渡します。

    多くの場合、CoffeeScript では括弧を省略することができるため、式が簡潔になります。例えば、square sum numssquare(sum(nums)) に相当します。2 番目の部分式は、配列の map メソッドを呼び出します。これも JavaScript 1.8 のメソッドであり、入力として別の関数を取ります。そして、この関数を配列に含まれる各要素に適用し、その結果から新しい配列を作成します。リスト 1 の例では、map への入力引数として square 関数を使用しているため、作成される新しい配列の要素はすべて、入力配列を二乗した値となります。したがって、この新しい配列を sum 関数に渡すだけで、二乗数の和を取得できるというわけです。

  4. Problem 6 を解決するために、範囲 [1..100] を指定して、適切な数値の配列を diff 関数に渡します。

    この範囲指定は、1 から 100 までのすべての数値 (1 と 100 を含む) の配列に相当します。もし 1 と 100 を範囲から除外したいとしたら、ピリオドを 2 つではなく、3 つ使用して [1...100] と表記します。このようにして指定した範囲を diff に渡せば、Problem 6 の解が得られます。

上記コードで説明した関数や範囲指定に加え、もう少し他の機能を紹介するために、今度は Project Euler の Problem 1 を検討します (「参考文献」を参照)。この問題で答えるのは、1000 未満の整数のうち、3 または 5 のいずれかで割り切れるすべての整数の和です。皆さんは、Project Euler のなかで最も簡単な問題だと思うことでしょう。Problem 6 と同じように、関数と範囲指定を使用するだけで簡単にこの問題を解くこともできますが、CoffeeScript の内包機能を使用すれば、リスト 2 のように洗練されたソリューションを作成することができます。

リスト 2. 内包を使用して Problem 1 を解く
coffee> (n for n in [1..999] when n % 3 
== 0 or n % 5 == 0).reduce (x,y) -> x+y
233168

1 行のコードで問題を解決できるというのは、いつでも気持ちの良いものです。これが可能なのは、CoffeeScript の簡潔な構文のおかげです。リスト 2 のソリューションは内包を使用して、3 または 5 の倍数であるすべての整数のリストを作成します。このリストは、最初に [1..999] の範囲を指定して生成されますが、リストに使用される値は 3 または 5 で割り切れる値のみです。これらの値を加算するために、ここでも reduce が使用されています。REPL はこの 1 行のコードを評価して、問題の解を出力します。

次のセクションでは、もう少し難しい問題に挑戦することによって CoffeeScript についてさらに詳しく探ります。


ブロック文、配列、ファイル

Project Euler の Problem 4 (「参考文献」を参照) は、2 つの 3 桁の数値の積で表される回文数のうち、最大の回文数を見つけるという問題です。この問題を解くにはさまざまな方法があります。リスト 3 は、その一例です。

リスト 3. 回文数をテストする
isPal = (n) -> 
  digits = [] 
  while n > 0 
    digits.push(n % 10) 
    n = Math.floor(n / 10) 
  len = digits.length 
  comps = [0..len/2].map (i) -> digits[i] == digits[len-i-1] 
  comps.reduce (a,b)-> a && b 

vals = []
vals.push(x*y) for x in [100..999] for y in [100..999]
pals = vals.filter (n) -> isPal(n)
biggest = pals.reduce (a,b) -> Math.max(a,b)
console.log biggest
  1. 数値が回文数であるかどうかをテストする、isPal という名前の関数を定義します。この関数は、これまでに定義した関数よりも少し複雑で、関数の本体には 7 行のコードが含まれます。

    お気付きかもしれませんが、CoffeeScript では波括弧 ({ }) やその他の明示的な方法を使用して関数の始まりと終わりを示すことはせず、Python と同じようにホワイトスペース (インデント) を使用します。関数の定義は、引数リストの後に右向き矢印 (->) を続けるという表記で開始します。isPal 関数では、数値を構成する各桁の数字のために空の配列を作成し、while ループを開始します。このループは JavaScript (あるいは、C や Java など) の while ループと同様です。ループの述部 (n > 0) を括弧で囲む必要はありません。ループの本体はインデントされて、これがループの一部であることが示されます。ループ内部では、引数として渡された数値の最後の桁の数字を配列の先頭に格納します。次に、数値を 10 で除算して、除算の余りを破棄します。これを繰り返すことによって、元の数値の各桁の数字からなる配列が出来上がります。別の方法として、このループを単純に digits = new String n に置き換えて、n を文字列に変換することもできます。その場合、コードの残りについては現状のままで機能します。

  2. 数字の配列 digits を作成した後は、この配列の半分の長さの配列を作成して map 関数を使用することで、半分の長さの配列のすべての要素について以下の処理を行います。つまり、半分の長さの配列の i 番目の要素を、元の配列で先頭から i 番目の要素と末尾から i 番目の要素に同じ値が格納されているかどうかを示すブール値に変換します。すべての要素の結果が true であれば、回文数ということになります。この完成したスクリプトをテストするには、ここでも単純に reduce 関数を使用しますが、この場合にはブール値をまとめて AND 演算します。
  3. isPal 関数の定義が完了したら、今度はこの関数を使用して回文数であるかどうかをテストします。2 つの 3 桁の数値の積であるすべての数値をテストします。
    1. 3 桁の数値の最小 (100) から最大 (999) までを範囲とする 2 つの内包を作成します。
    2. 内包ごとに、積を取って配列に入れます。
    3. 別の reduce 関数を使用して、配列に含まれる要素のうち、最大のものを見つけます。最後に、console.log を使用してこの要素を出力します。
    4. この出力をファイルに保存します。

リスト 4 に、このソリューションを実行して時間を計測する方法を示します。

リスト 4. Problem 4 のソリューションを実行する
$ time coffee p4.coffee
906609

real	0m2.132s
user	0m2.106s
sys	0m0.022s

リスト 4 に記載されているスクリプトは、高速なコンピューターでも実行するのに 2 秒以上の時間がかかります。これは主に、テストする 900*900 = 810,000 個の値 (その多くは重複しています) を生成する複合内包が原因となっています。追加の演習として、リスト 3 のコードを最適化してみてください (ヒント: 数値を文字列に変換すると、実際にかなり時間が短縮されます)。

Project Euler の Problem 22 (「参考文献」を参照) では、文字列のリストで複雑な計算を行うことが求められます。この問題はファイルからリストを読み取って、その内容を構文解析してリストにし、そのリストをソートして各文字列を数値に変換した後、各数値とリスト内での位置を表す数字の積を合計するという内容です。この Problem 22 では、CoffeeScript でファイルがどのように機能するかを知ることができます。また、この問題のソリューションには、文字列操作や配列に関するマジックも含まれます。リスト 5 にそのソリューションを記載します。

リスト 5. CoffeeScript でファイルを操作する
path = "/path/on/your/computer/names.txt"
fs = require "fs"
contents = fs.readFileSync path, "utf8"
strs = contents.split(",")
names = strs.map (str) -> str[1 .. str.length - 2]
names = names.sort()
vals = names.map (name) -> name.split("")
vals = vals.map (list) ->
	list.map (ch) -> 1 + ch.charCodeAt(0) - 'A'.charCodeAt(0)
vals = vals.map (list) ->
	list.reduce (a,b) -> a+b
vals = ((i+1)*vals[i] for i in [0...vals.length])
total = vals.reduce (a,b) -> a+b
console.log total
  1. ファイルを保存します。問題の説明文の中に、このファイルへのリンクがあります。あるいは、この記事に付属のソース・コードを使用することもできます。path 変数の値は、ご使用のコンピューター上でのファイルの絶対パスに変更してください。

    CoffeeScript には、ファイルを操作するための特殊なライブラリーは組み込まれていません。代わりに、CoffeeScript は node.js とその fs モジュールを利用します。require 関数を使用して、fs モジュールをロードしてください。

  2. fs.readFileSync を使用してファイルの内容を読み取ります。このファイルの内容は、"MARY", "PATRICIA", のようになっています。このように、始めは一続きの文字列となっているので、split メソッドを使用してコンマの箇所で分割します。

    各文字列は、まだ二重引用符 (") で囲まれた状態です。そこで、二重引用符を取り除くために map 関数を使用して、スライス処理によって各文字列を置換します。str が文字列だとすれば、str[1 .. str.length -2] は先頭から 2 番目の文字で始まって末尾から 2 番目の文字で終わる部分列です。したがって、コードは正確に最初と最後の文字、つまりうっとうしい二重引用符を取り除きます。このように、文字列のスライス処理は非常に重宝することがあります。

  3. 二重引用符が取り除かれた文字列のリストが準備できたら、次はリストをソートするために、配列の sort メソッドを使用します。続いて、文字列の各文字をアルファベットの先頭から何番目の文字であるかを示す番号に変換することで (A は 1、B は 2、C は 3 にするといった具合です)、文字列を数値に変換する必要があります。その方法は、以下のとおりです。
    1. 再度 split メソッドを使用して、各文字列を文字の配列に変換します。
    2. charCodeAt メソッドを使用して、各文字を数値にマッピングします。
    3. reduce による演算処理を行って、これらの数値を合計します。
    これで、文字列のリストは数値のリストに変換されます。
  4. 各数値をリスト内での出現順に乗算し、これらの積を別の内包を使用して加算します。新しい配列を作成し、この配列に、前の配列の要素を出現順に掛け合わせて生成した要素を取り込みます。そして、再び reduce による演算処理を行ってこの配列の要素を加算し、その結果を出力します。

今回も同じく、このスクリプトをファイルに保存すれば、後からファイルを実行して時間を計測することができます (リスト 6 を参照)。

リスト 6. Problem 22 の実行時間を計測する
$ time coffee p22.coffee
871198282

real	0m0.133s
user	0m0.115s
sys	0m0.013s

Problem 22 のソリューションを実行するのにかかった時間は 0.2 秒を下回っています。つまり、計算する内容を記述する英語の行数と、計算を実行するためのコードの行数はほとんど同じでした。これは、CoffeeScript の簡潔な構文を示す好例です。他のプログラミング言語では、上記のサンプル・コードよりも遥かに多くのコード行が必要になることは想像できるはずです。

このセクションでは、難易度の高い問題を CoffeeScript の重要な機能のいくつかを使って解決しました。次のセクションでは、CoffeeScript のなかでも極めて重要な機能について検討します。それは、オブジェクト指向プログラミングの機能です。


オブジェクト指向の CoffeeScript

第 1 回で述べたように、JavaScript の大きな欠点はその「他とは異なる」オブジェクト指向プログラミング (Object-Oriented programming: OOP) のスタイルにあります。JavaScript の OOP が他と異なるのは、JavaScript とは違って一般にはクラス・ベースの OOP が圧倒的多数を占めているためです。CoffeeScript はクラス・ベースの OOP になっています。

次の課題は、CoffeeScript の OOP を使って Project Euler の Problem 35 (「参考文献」を参照) を解くことです。Problem 35 では、循環素数とは、桁を「ローテート」させても常に素数を得られるという特性を持つ特殊な素数であると説明しています。リスト 7 のコードは、OOP を使って 100 万未満の循環素数の個数を計算します。

リスト 7. 循環素数をカウントする
class PrimeSieve 
  constructor: (@max) -> 
    @nums = [2..@max]
    for p in @nums 
      d = 2*p 
      while p != 0 and d <= @max 
        @nums[d-2] = 0 
        d += p 
  isPrime: (n) -> @nums[n-2] != 0
  thePrimes: -> @nums.filter (n) -> n != 0

class CircularPrimeGenerator extends PrimeSieve
  genPerms = (num) -> 
    s = new String num 
    x = (for i in [0 ... s.length] 
      s[i+1 ... s.length].concat s[0..i]) 
    x.map (a) -> parseInt(a)
  isCircularPrime : (n) -> 
    perms = genPerms(n)
    len = perms.length
    primePerms = perms.filter (p) => @isPrime(p)
    len == primePerms.length
  theCircularPrimes: ->
    (p for p in @thePrimes() when @isCircularPrime(p))  
	
max = process.argv[2]
generator = new CircularPrimeGenerator max

console.log "Number of circular primes less than #{max} is 
#{generator.theCircularPrimes().length}"
  1. PrimeSieve という名前のクラスを作成します。このクラスが実装するアルゴリズムは、特定の値未満のすべての素数を計算するための有名なエラトステネスの篩 (ふるい) です。

    クラスのプロパティーを意味する @ 記号は、「this.」の省略形でもあります。したがって、@nums = [2..@max] は、this.nums = [2 .. this.max] に相当します。

    クラスのメソッドを示すには、メソッドの名前の後にセミコロンと関数定義を続けます。最初の constructor というメソッドは、クラスのコンストラクターです。例えば、新しい PrimeSieve(100) が生成されると、このコンストラクターが呼び出され、100 が max として渡されて this.max にはこの値が設定されることになります。このサンプル・コードのコンストラクターは、篩を作成し、素数を @nums メンバー変数に格納します。続いてこのコードは、isPrimethePrimes という 2 つのメソッドを宣言しています。thePrimes は、配列フィルターを使用して @nums から合成数を除去するメソッドです。

  2. PrimeSieve のサブクラスである CircularPrimeGenerator を宣言します。CoffeeScript は、よく使われている多くの OOP 言語と同じく、class ... extends 構文を使用します。このサブクラスは、PrimeSieve のコンストラクター、メンバー変数、およびメソッドを継承し、以下のメソッドを実装します。
    • genPerms メソッド: 特定の数値の数字をローテートさせて、その数値のすべての順列を生成するメソッドです。
    • isCircularPrime メソッド: 特定の数値のすべての順列を生成し、順列のリストから素数でない数値を除去するメソッドです。フィルタリング後のリストに含まれる要素のすべてがフィルタリング前のリストと変わらない場合、その数値は循環素数であることになります。
    • theCircularPrimes メソッド: 内包を使用してすべての循環素数のリストを生成するメソッドです。
    ここで注目すべき点は、スーパークラスに定義された @thePrimes メソッドを使用すれば、単純に循環素数でない素数をフィルタリングして除去できることです。

    2 つのクラスを定義した後は、これらのクラスを使って問題を解きます。

  3. リスト 7 は、素数と循環素数の計算に使用する最大値として、コマンドラインの引数を取るように作成されています。process.argv を使用すれば、コマンドラインのすべての引数にアクセスすることができます。この配列の最初の 2 つの値はコマンドとスクリプトです。したがって、スクリプトで使用する最初のパラメーターを含んでいるのは process.argv[2] になります。

    スクリプトに渡された最大値を使用して、CircularPrimeGenerator のインスタンスを作成します。

    console.log を使って、見つかった循環素数の個数を出力します。

このサンプル・コードでは、CoffeeScript のもう 1 つの便利な機能を使用しました。それは、console.log に渡す文字列を作成するために使っている、文字列の補間機能 (文字列内での式展開) です。


まとめ

この記事では、CoffeeScript が持つ機能の数々を探りました。CoffeeScript の簡潔な構文と関数型プログラミングの機能によって、よく使われているアルゴリズムの多くを簡潔に実装することができます。それと同時に、CoffeeScript では単純化されたオブジェクト指向プログラミングを行うことも可能です。解決しなければならない問題の種類が何であれ、CoffeeScript の構文によってタスクが単純になることがわかるはずです。

連載の次回の記事では、さらに実際的な取り組みとして、CoffeeScript を使って Web アプリケーションのクライアント・サイドのスクリプトを作成します。お楽しみに。


ダウンロード

内容ファイル名サイズ
Article source codecs2.zip19KB

参考文献

学ぶために

製品や技術を入手するために

  • Node.js: Node.js をダウンロードしてください。スケーラブルなネットワーク・プログラムを容易に構築できるようになります。
  • IBM 製品の評価版: DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=799817
ArticleTitle=CoffeeScript の最初の一杯: 第 2 回 実践的な例から CoffeeScript 言語について学ぶ
publish-date=03082012