CodeIQ MAGAZINECodeIQ MAGAZINE

Direct Manipulationとプログラミング環境をScalaで書いてみる─by Scala福岡2017

2017.09.19 Category:勉強会・イベント Tag:

  • 10
  • このエントリーをはてなブックマークに追加

Scalaスピーカーが集結した「Scala福岡2017」。2番目に登壇したのは、グルーブノーツの花田恒一氏。セッションテーマは「Direct Mnipulationとプログラミング環境をScalaで書いた話」。 by 馬場美由紀 (CodeIQ中の人)

DMに関心を持った背景

福岡を拠点に高速分散処理技術を生かしたスマートデバイス×ビッグデータ領域に強みを持つスタートアップ、グルーブノーツのRockstarエンジニア、花田恒一氏は、Direct Mnipulationとプログラミング環境をScalaで実装した話を発表した。

Direct Manipulation(DM)とは、Shneidermanによって提唱された直感的、身体的に操作でき、すぐに結果がフィードバックされるインタラクションのスタイル。

現在、ほとんどのUIはDMとなっている。DMでない代表例は、コマンドラインの操作。実際にキーを叩く動作があるので、直感的ではない。

一方DMである例とは、新規フォルダーを作ってそこにドラッグ・アンド・ドロップでファイルを入れたり、ゴミ箱に捨てるという操作などだ。

なぜ、DMに関心を持っているか。元々花田氏はFLOWerというビジュアルプログラミング環境の開発に携わっていた。

株式会社グルーヴノーツ 花田恒一氏

ビジュアルプログラミング環境には同じく日本人が作ったViscuitもあるが、いずれもキーボードの操作に慣れていない子どもたちがプログラミングを始められるために作られた開発環境。テキストを書くことなく、絵を描いて配置すればプログラムができる。

現在はDM+CodeそしてCode+DMというように、プログラムをキーボードやマウスを使わずに開発する環境を開発している。その一例がScalaで設計・実装しているsigHexだ。

キーボードやマウスを使わずに開発する環境として重要なインタラクションの一つに、ジェスチャーがある。ジェスチャーを使うことで、ボタン減らしするなど、うまくインタラクションデザインすると使いやすくなる。特にタッチUIでは本当に重要な要素で、Direct Manipulationに貢献してくれる。

ただ、ジェスチャーには欠点がある。それはデザインや実装が難しいこと。そして見えづらいのでデバッグしにくいことだ。

ジェスチャーとひと言で言っても、タップやダブルタップ、ピンチイン、ピンチアウト、フリック、ホールドなどいろんな操作がある。

次にジェスチャーの可能性について。タップとダブルタップ、フリック、ホールド、スクロールなど指の動きがあるところまで重複している操作がいくつかあるが、どう区別するかわかりにくい。

それらのイベントを見分けるために、DFA(決定性有限オートマトン)やNFA(非決定性有限オートマトン)など複数の可能性をチェックする必要がある。

それができればパーサの文字列の解釈とジェスチャーのイベントの解釈は似ているので、パーサコンビネータ(パーサ同士を組み合わせて新しいパーサをつくる技法)で楽に書けるのではないかと考えたというわけだ。

パーサの文字列解釈とジェスチャーのイベント解釈は似ている

簡単なモデルで紹介、ある入力の文字列”(10 20 30)”をパーサに入力して、List(10,20,30)と出力する。これをScalaで書くと、次のようになる。

abstract class Parser[+A] extends
(String   => A)

さらに続きの文字列があるとする。それをScalaで書くと次のようになる。

abstract class Parser[+A] extends
(String   =>  (A, String))

図の様な空白で区切っているような文字列だと、エラーになる。それを考慮してScalaで書くと、Resultという型とcase classを作ることになる。

abstract class Parser[+A] extends String   =>  Result[A]

abstract class Result[+A]
case class Success[+A](item:A,next:String)  extends Result[A]
case class Error(msg:String) extends Result[Nothing]

Successの場合は続きの文字列を出力する。Errorの場合は、メッセージを投げるようなcase classとなる。

ここまで書くと、パーサのマップの生成が考えられる。例えば(10,20,30)を入力すると、List出力した値をsumという関数で返す。これは新しいパーサができることになる。これをScalaで書いてみると次の図のようになる。

abstract class Parser[+A] extends (String   =>  Result[A]) { p =>
    def map[B](f: A => B) : Parser[B] = new Parser[B]{
          def apply(input: Text) : Result[B] = 
                p(input) match {
                     case Success(a, next) => Success(f(a), next) 
                     case Error(msg) =>  Error(msg)
                }
    }
}

parserA map (x => x + 1)

Aというパーサの出力をBに出力を変えるというイメージだ。

続いてパーサのフラットマップを考える。パーサがあって、fratmapという関数がある。Aを取ってBを出力するパーサを返す関数がある。上の方は成功する文字列がくると、パーサ2という関数ができる。パーサ2に対して続きの文字列を適用することで、続きの文字列を出力する。エラーはエラーとなる。それをScalaで書くと次のようになる。

abstract class Parser[+A] extends (String   =>  Result[A]) { p =>
    def flatMap[B](f: A => Parser[B]) : Parser[B] = new Parser[B]{
          def apply(input: Text) : Result[B] = 
                p(input) match {
                     case Success(a, next) =>  
                         val newParser = f(a)
                          newParser(next) 
                     case Error(msg) =>  Error(msg)
                }
    }
}

かなり複雑な文字列となるが、先との違いはAをとってパーサーBを返す関数となる。成功したら新しいパーサを作る。ニューパーサに続きの文字列を入れて結果を返す。エラーになるとエラーをそのまま返す。

ここまで書くとパーサとパーサを逐次的に組み合わせることができる。

” (10, 20, 30) concat (… ” という文字列から、”(10,20,30)”をパースして、”List(10,20,30)と”concat (…”という残りの文字列を返す。そして、”concat (…”という文字列から、Concatと”(..”という残りの文字列を返すパーサがあり、それを組み合わせた構文があるとする。

この図を書き直すと、このようになる。長方形が1つのパーサに見えるようになる。

これをScalaで書くと次のようになる。

abstract class Parser[+A] extends (String => Result[+A]) { p1 =>
    def ~[B](p2: Parser[B]) : Parser =  new Parser[(A,B)] {
       val apply(input: String) => Result[(A,B)] =  {
              val p3 = 
                for{ a <-  p1
                      b <- p2
                 } yield (a,b)
              p3(input)
        }
   }
}

足し算のパーサは次のような実装になる。

case class Add(x: Double, y: Double)

val double : Parser[Double] = ...
val plus :  Parser[String] = .... 

val add : Parser[Add] = (double ~ plus ~ double) map { ((x, _), y)  => Add(x,y) }

ジェスチャーをパーサで表し、Scalaで書いてみる

ジェスチャーをパーサで表す。図のようにタップしたときの座標列を解釈して、MoveUpというデータを返すというふうになる。

Scalaで書くと次のようになる。

abstract class Parser[Input,+T] extends (Input=>Result[Input, T]) 

abstract class Result[Input, +T]
case class Success[Input, +T] (t: T, input: Input) extends Result[Input, +T]
case class Error[Input](msg: String) extends Result[Input, Nothing]

case class Vector2(x:Double, y:Double) 

class Gesture[+T] extends Parser[Vector2, T]

ジェスチャーを定義する。まずはInput型の定義をし、次に出力する型を定義する。その際、scala-parser-combinatorsを利用する。

入力シーケンスの要素の定義はこうなる。

case class Vector2(x: Double, y: Double) {
  def +(o: Vector2) = Vector2(x + o.x, y + o.y)
  def -(o: Vector2) = Vector2(x - o.x, y - o.y)
  def *(f: Double) = Vector2(x * f, y * f)
  def *(o: Vector2) = x * o.x + y * o.y
  def /(f: Double) = this * (1 / f)

  def magnitude = math.sqrt(x * x + y * y)
  def normalize = this / magnitude

  def angle = math.atan2(y, x)
}

ベクトルの計算がやりやすくなるようにしている。

scala-parser-combinatorsのパーサの定義をする。シーケンスを呼び出すような型がInputになる。

trait Parser {
    type Elem 
    type Input = Reader[Elem]

     ...

     abstract class Parser[+T] extends (Input => ParseResult[T])     
    ....

}

続いて、scala-parser-combinatorsのReader, Positionを定義する。

abstract class Reader[+T]  {
    def atEnd: Boolean
    def first: T
    def pos: Position 
    def rest: Reader[T]
   ....
}

trait Position {
  def line: Int
  def column: Int
  ....
}

GestureReader,GesturePositionを定義する。

class GestureReader( val data: List[Vector2], override val offset: Int ) extends Reader[Vector2] {
  def this( data: List[Vector2] ) = this( data, 0 )

  class GesturePosition( val offset: Int ) extends Position
  {
    override val line = 1
    override def column = offset + 1
    override def lineContents: String = ""
  }

  override def atEnd = offset >= (data.length - 1)
  override def first = data( offset )
  override def pos = new GesturePosition( offset )
  override def rest = new GestureReader( data, offset + 1 )
}

ジェスチャーの出力の型を定義する。

object Gesture{
  trait Atom {
    val pos : Vector2
  }
  object Atom {
    case class Idle(pos:Vector2) extends Atom
    case class Up(startPos:Vector2, pos:Vector2) extends Atom
    case class Down (startPos:Vector2, pos:Vector2) extends Atom
    case class Left (startPos:Vector2, pos:Vector2) extends Atom
    case class Right (startPos:Vector2, pos:Vector2) extends Atom
  }
}
case class Gesture(last:Gesture.Atom, history:List[Gesture.Atom])

ジェスチャー型にはhistoryがある。そのためAtomをパースするたびに、historyを更新する。その実装はパーサコンビネータの実装と変わらない感じだ。

ジェスチャーの実装は次の通り。

val pos : Parser[Vector2] = elem("POS", e => true)

def gesture(g:Gesture) : Parser[Gesture] =
    atom(g).flatMap(gesture) | atom(g)

def atom(g:Gesture) = up(g) | down(g) | left(g) | right(g) | idle

  def idle(g:Gesture) : Parser[Gesture] =
    (pos ^^ { p => (p,g)}).filter { ... }

  def move(g:Gesture) : Parser[Move] = { ..  }

  def up(g:Gesture) : Parser[Gesture] = ...

  def down(g:Gesture) : Parser[Gesture] = ..

  def left(g:Gesture) : Parser[Gesture]  = ...

 def right(g:Gesture): Parser[Gesture] = ...

1行目は位置を返している。

さらに高度なジェスチャーにするには、文字列ではなくてイベント列を作ることになるが、最終的にはup, down, left rightのみの構成となる。

historyをさらにパースしたり、時間管理を入れることになる。タップとダブルタップの解釈や複数の指の解釈などの実装も楽にできるようになる。

ジェスチャーとパーサコンビネータを再考すると、MVCのCと似ていることに気付く。ジェスチャーを課題にしてパーサを考える。その基本となるのは状態管理。それをいかに容易にするか、それを考えながら実装することが大切だと花田氏は最後に語った。

「Scala福岡2017」レポート特集

  • 10
  • このエントリーをはてなブックマークに追加

■関連記事

ヌーラボ福岡で開催された「Scala福岡2017」に参加してきました(セッションレポートまとめ)... 「Scala福岡2017」レポートまとめ Scalaを福岡・九州で盛り上げていきたいということで、福岡のヌーラボさんで開催された「Scala福岡2017」。 ▲ヌーラボ福岡のお隣には、安くて美味しい立食いすし屋さんも! 登壇したのは、Scalaのエキスパートなエンジニアの皆さん! Cod...
Scalaにまつわる疑問や誤解に一問一答!Scalaは難しくない!?─by Scala福岡2017... Scalaに関しては次のような噂がある ドワンゴ水島宏太さんは、Japan Scala Associationの代表理事。水島さんは自らを言語オタクと語り、Scala以外にもNemerle、Rust、Standard MLなど数々の言語を愛しているという。また、Klassicというプログラミング言...
Scalaプログラマから見た機械学習サーバー「Apache PredictionIO」とは?─by ... 機械学習の課題 竹添直樹さんはビズリーチでScalaのプログラマを務めながら、OSS開発や技術書(「Scalaパズル」「Scala逆引きレシピ」)の執筆、さらにはPredictionIOやGitBucket、Scalatraのコミッターを務めている。 今回は、セッションではScala製機械学習サ...
はてな粕谷氏が語る、安全なPlay Frameworkのバージョンアップのコツとは─by Scala... なぜ、バージョンアップが必要なのか ご存知の方も多いと思うが、サーバ管理・監視ツール「Mackerel」を直訳すると、「サバ」という意味。それをもじって名付けた。現在、粕谷氏は同ツール開発チームディレクターとして、開発メンバーをマネジメントしている。 ▲株式会社はてな Mackerel開発チーム...
Scala/Spark/Mahoutでレコメンドエンジンを作る─by Scala福岡2017... 今はデジタルマーケティングが中心。レコメンドエンジンが売上を左右する TechJINの創業は2016年10月。マーケティングプラットフォーム「b→dash」を開発するフロムスクラッチのグループ企業として、マーケティングテクノロジー領域の基礎研究・開発などの事業を展開している。 b→dashはMa...
これからScalaを始める人に役立つヒント満載!『Scala大名の平成維新☆殿中でScala!』... LT1:「十人十色のScalaの始め方」 トップバッターはナイルの佐藤和輝さん。現在はApplivのマイクロサービス化&検索機能改善に携わっている。Applivは「アプリインストールを、もっと身近なものにする」をミッションに、2012年8月リリースされたユーザー参加型のスマートフォンアプリ発見サー...

今週のPickUPレポート

新着記事

週間ランキング

CodeIQとは

CodeIQ(コードアイキュー)とは、自分の実力を知りたいITエンジニア向けの、実務スキル評価サービスです。

CodeIQご利用にあたって
関連サイト
codeiq

リクルートグループサイトへ