Direct Manipulationに、パーサコンビネータでジェスチャーを実装した話

by Nobkz

@Nobkz

  • はなだ のぶかず
  • 株式会社Groovenauts Rockstar Engineer
  • Scala/Shen/Rust/Erlangなど
  • ボードゲーム/デジタルゲーム/ika2
  • もともと音響屋 
  • 音響プログラミング言語を作成したりしてた

この登壇について

  • この登壇はDirect ManipulationのScala実装に関する登壇です。
  • パーサコンビネータが中心になります
  • ジェスチャーが楽しく実装できるようになるといいな!?

Direct Manipulation[Shneiderman, 1983]

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

DMである例、そうでない例

Visual Programming Language 

  • FLOWer
  • Viscuit

DM + Code & Code + DM

  • Sketch-n-Sketch
  • SigHex

ジェスチャーについて

ジェスチャーの重要性

  • ボタンを減らす
  • うまくインタラクションデザインすると使いやすくなる
  • タッチUIではホントに重要
  • Direct Manipulationに貢献する

ジェスチャーの欠点

  • デザインが難しい
  • 実装が難しい
  • 見えづらいので、デバッグしにくい

ジェスチャーの種類

  • タップ、 ダブルタップ
  • ピンチイン、 ピンチアウト
  • フリック
  • ホールド
  • etc ...

ジェスチャーの可能性

  • タップとダブルタップ、フリック、ホルード、スクロール
  • ピンチとスクロール
  • どう区別するか?

イベントを見わける

  • 複数の可能性をチェックする
    • DFA (決定性有限オートマトン)
    • NFA (非決定性有限オートマトン)
  • パーサの文字列の解釈とジェスチャーのイベント解釈は似ている
  • パーサコンビネータで楽に書けるのでは?

パーサコンビネータ

パーサ同士を組み合わせて新しいパーサをつくる技法。

パーサ

Parser

"(10 20 30)"

List(10,20,30)

入力文字列

出力データ

Scalaで書くと...

abstract class Parser[+A] extends

(String   => A)

Parser

Parser

"(10 20 30) (....  "

List(10,20,30)

+

"(..."

入力文字列

出力データ

+

続きの文字列

Scalaで書くと...

abstract class Parser[+A] extends

(String   =>  (A, String))

エラーが出るかもしれない

"(10, 20,  "

Error!!!

Parser

"(10 20 30) (....  "

List(10,20,30)

+

"(..."

Scalaで書くと

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]

パーサのmap

"(10, 20,  "

Error!!!

Parser

"(10 20 30) (....  "

List(10,20,30)

+

"(..."

sum

60

f

パーサのmap

"(10, 20,  "

Error!!!

Parser

"(10 20 30) (....  "

List(10,20,30)

+

"(..."

sum

60

+

"(.."

f

パーサのmap

Parser

sum

f

map

=

New Parser!!

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)

パーサのflatMap

Parser

flatMap

f

A

Parser

パーサのflatMap

Parser1

List(10,20,30)

+

"(..."

f

Parser2

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)
                }
    }
}


パーサを逐次的に組み合わせる

Parser1

"(10 20 30)

concat (....  "

List(10,20,30)

+

"concat (..."

Parser2

"concat (..."

Concat

+

"(..."

Parser

"(10 20 30)

concat (....  "

List(10,20,30)

+

"concat (..."

Concat

+

"(..."

パーサを組み合わせる

Parser

(List(10,20,30)

+

Concat)

+

"(..."

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) }

ジェスチャーとパーサ

Parser

"(10 20 30)"

List(10,20,30)

入力文字列

出力データ

ジェスチャーとパーサ

Parser

List(10,20,30)

Seq[A]

出力データ

出力列

ジェスチャーとパーサ

Parser

MoveUp

Seq[(Double,

Double)]

出力データ

出力列

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のParserの定義

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 )
}

Gestureの出力の型を定義

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])

ジェスチャー履歴の更新

case class Gesture(last:Gesture.Atom, history:List[Gesture.Atom])
  • Gesture型には、historyがある
  • Atomをパースするたびに,histroyを更新する。
(currentGesture:Gesture)=>(atom:Atom)=>Gesture

ジェスチャーの実装

 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] = ...

さらに高度なジェスチャーへ

  • up, down, left rightのみで構成している
  • historyを更にパースする
  • 時間管理
  • タップとダブルタップの解釈
  • 複数の指の解釈

ジェスチャーと

パーサコンビネータ再考

  • なにかに似ている..?
  • MVCのC
  • Rx系との比較(時間がなさそう)

Direct Manipulation

By Nobukazu Hanada

Direct Manipulation

  • 4,649