Scala プログラミング勉強メモ (2)

最近購入した「Scalaプログラミング入門」というご本の勉強メモ。

Hello World!

おなじみの Hello World! と出力するコード。

println("Hello World!")

scala コマンドにファイル名を渡してあげることで、実行することができる。

$ scala HelloWorld.scala
Hello World!

変数の定義

変数の定義は、varvallazy val キーワードを使用する。

var 変数は、一度値を代入した後、別の値を再代入することができる。

var foo: String = "Foo"

val 変数は、一度しか代入できない。また、変数が含まれているコードブロックが実行されたときに評価される。

val bar: String = "Bar"

lazy val 変数は、一度しか代入できない。また、変数にアクセスしたときに評価される。

lazy val baz: String = someLongQuery()

特別な理由がない限り、val 修飾子を使うのがよいそう。

また、Scala の型推論は優秀なのだそうで、変数の型が明白な場合は型の宣言を省略したほうがコードがすっきりする。

var foo2 = "Foo"
var bar2 = "Bar"

メソッドの定義と呼び出し

メソッドの定義は def キーワードを使用する。
以下は Hello World! と出力するプログラムをメソッドを使って実装した例。

def HelloWorld(): String = {
  "Hello World!"
}

println(HelloWorld) // => Hello World!

def キーワードに続いて、メソッド名、引数、戻り値の型、= キーワード、メソッド本体を記述する。
HelloWorld メソッドは引数なし、String を返すメソッド。

メソッドの呼び出しは () 内に引数を渡すことで実行できる。
また、引数がない場合は () を省略できる。
HelloWorld メソッドは引数をとらないので、呼び出すときは () を省略できる。

また、メソッドの定義は 1 行で記述することも可能。

def HelloWorld(): String = "Hello World!" 

以下は、引数をとるメソッドの例。
() 内に 仮引数: 型名 という形式で記述する。

def foo(a: Int): String = a.toString 
def bar(a: int, b: Boolean): String = if (b) a.toString else "false"

引数が 1 つのメソッドは、() の代わりにスペース区切りで呼び出すこともできる。

foo(1) // => String = 1
foo 1  // => String = 1

Scala プログラミング勉強メモ (1)

最近購入した「Scalaプログラミング入門」というご本の勉強メモ。

Scala のインストール

svm というバージョン管理ツールが便利そうだったので使ってみることに。
とりあえず $HOME/bin に置いておくことに。

$ curl "https://raw.github.com/yuroyoro/svm/master/svm" -o ~/bin/svm
$ chmod 755 ~/bin/svm

.bashrc.zshrc に以下の行を追加しておく。

export SCALA_HOME=~/.svm/current/rt
export PATH=$SCALA_HOME/bin:$PATH

あとはインストールするだけ。

$ svm install 2.10.3
$ svm current
currently version is 2.10.3

REPL を使ってみる

scala コマンドで REPL を使うことができる。

$ scala
Welcome to Scala version 2.10.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_65).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

足すぜ

scala> 1 + 1
res0: Int = 2

引くぜ

scala> 3 - 1
res1: Int = 2

掛けるぜー

scala> 2 * 3
res2: Int = 6

割るいぇあ!

scala> 8 / 2
res3: Int = 4

Google Calendar API の Push Notification 機能を使ってみた

Google Calendar API の Push Notification 機能を使うと、Google カレンダー側の更新をリアルタイムに自分のアプリケーションに反映することができるらしいので試してみました。

Push Notification 機能を使うためには、通知を受け取る URL を Google 側に事前に登録しておく必要があるのですが、手順はドキュメントに書いてあるため省略します。

今回、クライアント用のライブラリには google-api-client を使用します。

# Gemfile
gem 'google-api-client'

Channel を作成する

Push 通知を受け取るために、まず Channel と呼ばれるものを作成する必要があります。

変更の通知を受け取りたいカレンダーを指定して Channel を作成することで、そのカレンダー内のイベントが変更されるたびに、Google 側から HTTPS でリクエストが飛んでくるようになります。

ちなみに Channel は変更の通知を受け取りたいカレンダーごとに作成する必要があるようです。

require 'google/api_client'

client = Google::APIClient.new

client.authorization.client_id = '<CLIENT_ID>'
client.authorization.client_secret = '<CLIENT_SECRET>'
client.authorization.refresh_token = '<YOUR_REFRESH_TOKEN>'
client.authorization.fetch_access_token!

service = client.discovered_api('calendar', 'v3')

client.execute!(
  api_method: service.events.watch,
  parameters: { calendarId: '<YOUR_CALENDAR_ID>' },
  body_object: {
    id: '<CHANNEL_ID>',
    type: 'web_hook',
    address: '<YOUR_RECEIVING_URL>'
  }
)

<CLIENT_ID><CLIENT_SECRET>Google Cloud Console で確認できます。

<YOUR_REFRESH_TOKEN>OAuth 2.0 認証後に取得できる refresh_token を指定します。

<YOUR_CALENDAR_ID> に変更の通知を受け取りたいカレンダー ID を指定します。
ちなみに、ここで自分のカレンダー ID を調べることができます。

<CHANNEL_ID>には任意の文字列を指定します。
複数の Channel を作成する場合は Channel 間でユニークになっているほうがいいでしょう。
ちなみに、自分はSecureRandom.uuid()を使っています。

<YOUR_RECEIVING_URL> には通知を受け取りたい URL を指定しておきます。

リクエストに問題がなければ、以下のようなレスポンスが返ってきます。

{
  "kind": "api#channel",
  "id": "4a0d04ac-b5df-4a4d-ba03-65d121fbc100",
  "resourceId": "ROKWIDXHB6wV6KdPqFWmo_lTTLF",
  "resourceUri": "https://www.googleapis.com/calendar/v3/calendars/sei@me.com/events?alt=json",
  "expiration": "1384317818000"
}

ここで大事なのはidexpirationです。

idは先ほど指定した<CHANNEL_ID>の値が入ってきます。

expirationは Channel の有効期限で、これが切れると通知が飛んでこなくなります。
もし、引き続き通知を受け取りたい場合は、期限が切れる前に Channel を作成し直す必要があります。

Push Notification を受け取る

Channel 作成後、カレンダーに変更があると以下のようなヘッダー付きのリクエストが飛んできます。

HTTP_X_GOOG_CHANNEL_EXPIRATION: Sun, 10 Nov 2013 11:08:46 GMT
HTTP_X_GOOG_CHANNEL_ID: 4a0d04ac-b5df-4a4d-ba03-65d121fbc100
HTTP_X_GOOG_MESSAGE_NUMBER: 235226900
HTTP_X_GOOG_RESOURCE_ID: ROKWIDXHB6wV6KdPqFWmo_lTTLF
HTTP_X_GOOG_RESOURCE_STATE: exists
HTTP_X_GOOG_RESOURCE_URI: https://www.googleapis.com/calendar/v3/calendars/sei@me.com/events?alt=json

HTTP_X_GOOG_CHANNEL_IDは先ほど登録した<CHANNEL_ID>です。

通知では変更の内容までは教えてくれないので、通知が来る度に API を使って更新された内容を取得する必要があります。

更新されたイベントを取得する

<YOUR_CALENDAR_ID>にカレンダー ID を指定して更新があったイベントを取得します。
複数の Channel をもつアプリケーションを構築する場合は、通知内のHTTP_X_GOOG_CHANNEL_IDの値をもとにカレンダー ID を参照できるようにしておく必要があります。

require 'google/api_client'

client = Google::APIClient.new

client.authorization.client_id = '<CLIENT_ID>'
client.authorization.client_secret = '<CLIENT_SECRET>'
client.authorization.refresh_token = '<YOUR_REFRESH_TOKEN>'
client.authorization.fetch_access_token!

service = client.discovered_api('calendar', 'v3')

client.execute!(
  api_method: service.events.list,
  parameters: {
    calendarId: '<YOUR_CALENDAR_ID>',
    updatedMin: 1.minute.ago.to_datetime.rfc3339
  }
)

変更があったイベントをピンポイントで取得できるわけではないので、直近で変更があったイベントを検索する形になります。

ヘタすると取りこぼしの危険があるので、多めに 1 分前以降の変更を取得するようにしていますが、もっといい方法があればなぁ、という感じです。
(Google さん、変更があった時刻も教えてくれるとうれしいですっ!)

Backboneのテンプレート開発は、Grunt使ってJSTに変換すると捗る

Backbone使うときにちょっと考えないといけないのが、テンプレートをどこに書くかという問題です。

「ToDoアプリを作ってみよう!」的なチュートリアルは、大抵、Underscore.jsのテンプ レートをindex.htmlのなかに書いていくパターンが多いかと思います。

こんな感じに。

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Backbone Tutorial</title>
</head>
<body>
  ...

  <script type="text/template" id="item-template">
      ...
  </script>

  ...
</body>
</html>  


Backboneからは以下のように、jQueryを使ってテンプレート化します。

var itemView = Backbone.View.extend({
  ...

  template: _.template($('#item-template').html()),

  ...
});

これ、テンプレートが少ないうちはいいのですが、多くなってくると管理が大変です。

テンプレートはテンプレートとして別のファイルに分けて開発したくなりますね。
かと言ってファイルが多くなると、それはそれでhtmlから読み込むのが面倒だったり。。

Railsとか使っていると、Backboneのgemを入れればそのへんの環境もまとめて整えてくれるのですが、クライアントサイドだけだと自分で環境を整えないといけません。

という状況で、Gruntが活躍してくれました。

プラグインのインストール

今回はcontrib-jstというプラグインを使いました。(次はcontrib-handlebarsを使ってみようと思います)
まずはそいつをインストールします。

$ npm install grunt-contrib-jst --save-dev

Gruntの環境がない方はこちらを参照

テンプレート用ディレクトリの準備

今回は以下のようなディレクトリ構成にしました。

|-- build
|   |-- images
|   |-- javascripts # ここにJSTファイルが置かれる
|   |-- stylesheets
|   `-- index.html
|-- src
|   |-- images
|   |-- javascripts
|   |-- stylesheets
|   |-- templates # ここにテンプレートファイルを置いておく
|   `-- index.html
|-- Gruntfile.js


Gruntfile.jsの編集

Gruntfile.jsというファイルをルートディレクトリに作成し、以下のように編集します。

module.exports = function(grunt) {

  grunt.initConfig({

    pkg: grunt.file.readJSON('package.json'),

    jst: {
      compile: {
        options: {
          templateSettings: {
            interpolate : /\{\{(.+?)\}\}/g
          },
          processName: function(filename) {
            return filename.replace(/(src\/templates\/|.html)/g, '');
          }
        },
        files: {
          'src/javascripts/templates/templates.js': [
            'src/templates/**/*.html'
          ]
        }
      }
    }

  });

  grunt.loadNpmTasks('grunt-contrib-jst');
}

これで環境は整いました。

src/templates 以下に .htmlという拡張子をつけてUnderscore.jsのテンプレート入れておけば、まとめてJSTファイルにしてくれます。
(今回は.htmlという拡張子をつけるようにしましたが、.tpmlでも何でもいいと思います)

ワークフロー

たとえば、hello.htmlを置いてコマンドを実行すると、build/javascripts以下にtemplates.jsというファイルが作成されます。

$ grunt jst


htmlでtemplates.jsを読み込んでおけば、Backboneからは以下のようにしてテンプレート使うことができます。

var helloView = Backbone.View.extend({

  template: JST['hello'],

  render: function() {
    this.setElement(this.template());
    return this;
  }

});


これですっきり!

Githubの隠し機能、Markdown記法のチートシート表示

もしかしたら常識なのかもしれませんが、Githubのページで「m」と打つとMarkdownのチートシートが表示されることを知った。
Readme.mdをブラウザから編集する時なんかに、たまに使うかも〜



f:id:se__i:20130428232652p:plain

Backboneの拡張、extendEachで複数のオブジェクトをextendする

Backboneを使っていると、あるCollectionの機能を別のCollectionでも使いたかったり、共通化したかったりする場面があります。

そんなときは、以下のようにextendメソッドでsubclassを作ることが多いと思います。

 

var ParentCollection = Backbone.Collection.extend(properties);

var ChildCollection = ParentCollection.extend({
  ...
});

 

引き継ぎたいpropertiesが1つである場合には上のようにすれば問題ありません。

ParentCollectionは、Backbone.Collection.extendで作成されていて、extendメソッドが使えます。

さらに、ParentCollectionのextendメソッドを使って、ChildCollectionにプロパティを引き継ぐこともできます。

 

ただ、extendメソッドにpropertiesは一つしか渡せず、2つ目以降はインスタンスプロパティではなく、クラスプロパティになります。

なので、複数のpropertiesを渡せるようにしたいときは、以下のようにBackboneを拡張します。

 

function extendEach() {
  var child = this
    , args = Array.prototype.slice.call(arguments)
    , len = args.length;

  return _.reduce(args, function(child, o, i) {
    return (i >= len - 1) ? child.extend(o) : child.extend(o.prototype);
  }, child);
}

Backbone.Collection.extendEach = extendEach;

 

Underscore.jsのreduce使って、順番にextendを実行していくメソッドです。

上の例では、Backbone.Collectionしか拡張していませんが、もちろんModelやView、Routerにも代入してあげれば、extendと同じ要領でextendEachが使えるようになります。

 

var GrundChildCollection = Backbone.Collection.extendEach(ParentCollection, ChildCollection, {
  ...
});

 

Backboneを拡張せず、グローバルな関数として作成したいときは以下のようにしてもいいかもしれません。

 

function extendEach() {
  var args = Array.prototype.slice.call(arguments)
    , len = args.length;

  return _.reduce(args, function(child, o, i) {
    return (i >= len - 1) ? child.extend(o) : child.extend(o.prototype);
  });
}

 

BackboneでPagination(ページャー機能)を実装

 

※ 2013-9-11 追記

Backbone.SimplePaginator というプラグインにして Github 上に公開しました。

 

BackboneでPagination(ページャー機能)の実装をしたいとなると、 backbone.paginatorという多機能なプラグインがあるのですが、多機能故に指定するオプションなどが多いので、向いていない場面もあります。

 

今回もうすこしシンプルでサクッと使えるプラグインが欲しかったので、backbone.paginatorのコードを参考に必要最低限の機能のプラグインを自前で作ってみました。

App.Collections.Paginator = Backbone.Collection.extend({

  currentPage: 1,
   
  perPage: 6,

  nextPage: function() {
    if (this.currentPage < this.information.totalPages) {
      this.currentPage = ++this.currentPage;
      this.pager();
    }
  },

  previousPage: function () {
    if (this.currentPage > 1) {
      this.currentPage = --this.currentPage;
      this.pager();
    }
  },

  goTo: function(page) {
    if (page !== undefined){
      this.currentPage = parseInt(page, 10);
      this.pager();
    }
  },

  goToFirst: function() {
    this.goTo(1);
  },

  pager: function() {
    var self = this
      , perPage = self.perPage
      , start = (self.currentPage - 1) * perPage
      , stop = start + perPage;

    if (!self.origModels) {
      self.origModels = self.models;
    }

    self.models = self.origModels.slice();
    self.reset(self.models.slice(start, stop));
  },

  info: function() {
    var self = this
      , currentPage = self.currentPage
      , totalPages = Math.ceil(self.origModels.length / self.perPage)
      , info;

    info = {
      currentPage: currentPage,
      perPage: self.perPage,
      totalPages: totalPages,
      lastPage: totalPages,
      previous: false,
      next: false
    }

    if (currentPage > 1) info.previous = currentPage - 1;
    if (currentPage < totalPages) info.next = self.currentPage + 1;

    info.pageSet = self.getPageSet();

    return info;
  },

  getPageSet: function() {
    var self = this
      , totalPages = Math.ceil(self.origModels.length / self.perPage)
      , pages = [];

    for (var i = 1, l = totalPages; i <= l; i++) {
      pages.push(i);
    }

    return pages;
  }

});

 

特徴としては、

・Collectionの拡張にすることで、Viewにロジックを書かなくて済む

・クライアント側で処理するので、サーバ側でPagination機能を実装する必要がない(静的なJSONファイル等にも対応できる)

・1ページに表示したいモデルをmodelsとしてもつので、ページ毎にchangeイベントが発火する

あたりでしょうか。

 

簡単な使い方ですが、まずperPageには1ページの表示数を指定しておきます。

PaginationさせたいCollectionは、以下のようにPaginatorを継承しておくようにします。

App.Collections.MovieList = App.Collections.Paginator.extend({

  initialize: function() {
    this.fetch();
    this.goToFirst();
  },

  ...

});

これでロジック側の準備は完了。

 

Viewはこんな感じ。初期化時にPaginatorを継承したCollectionを渡してあげます。

App.Views.Pagination = Backbone.View.extend({

  template: JST['pagination'],

  events: {
    'click a.prev': 'goToPrev',
    'click a.next': 'goToNext',
    'click a.page': 'goToPage'
  },

  initialize: function() {
    this.collection.on('reset', this.render, this);
  },

  goToPrev: function(e) {
    e.preventDefault();
    this.collection.previousPage();
  },

  goToNext: function(e) {
    e.preventDefault();
    this.collection.nextPage();
  },

  goToPage: function(e) {
    e.preventDefault();
    var page = $(e.target).text();
    this.collection.goTo(page);
  },

  render: function() {
    this.$el.html(this.template(this.collection.info()));
    return this;
  }

});

テンプレートはこんな感じ。

<span class="pages">
  <% if (currentPage != 1) { %>
    <a href="#" class="prev"><<</a>
  <% } %>
 
  <% _.each(pageSet, function(p) { %>
    <% if (currentPage == p) { %>
      <span class="page selected"><%- p %></span>
    <% } else { %>
      <a href="#" class="page"><%- p %></a>
    <% } %>
  <% }); %>
 
  <% if (lastPage != currentPage && lastPage != 0) { %>
    <a href="#" class="next">>></a>
  <% } %>
</span>

 

あとはCSSで調整すれば、以下のような素敵なPaginationが出来上がります。

f:id:se__i:20130421011050p:plain