ザネリは列車を見送った

ブログという名の備忘録

再帰関数を Collections.Seq 系関数に書き換える

前回のエントリの「標準入力から文字列を読み取り先頭から連続する数字だけを抜き出すプログラム」を
Collections.Seq 系関数で書き換えてみる。
takeWhile 関数が適しているだろう。

 

getNumIndex 関数を以下で置き換える。

Seq.takeWhile(fun s -> Char.IsNumber(s)) line

コレクションのままでは文字列として標準出力できないので、toArray 関数で配列に変換する。
Seq.takeWhile してから Seq.toArray すると、以下のいずれかのように書くようになるが、

// 一旦 takeWhile の結果を変数束縛してから toArray (ちょっと冗長…)
let numSeq = Seq.takeWhile(fun s -> System.Char.IsNumber(s)) line
Seq.toArray numSeq
// line に先に takeWhile を適用してから toArray (ちょっと読みづらい…)
Seq.toArray (Seq.takeWhile(fun s -> System.Char.IsNumber(s)) line)

ここでパイプライン演算子(|>)を使用して左から右に流れるように関数を適用するようにしてみる。

line |> Seq.takeWhile(fun s -> System.Char.IsNumber(s)) |> Seq.toArray

ちなみに関数合成演算子(>>)を使用すると以下のようになるが、

line |> (Seq.takeWhile(fun s -> System.Char.IsNumber(s)) >> Seq.toArray)

ここはパイプライン演算子で次々に関数を適用していく書き方のほうが可読性が高そう。

 

最終的にこんな感じになった。
また、別パターンとして tryFindIndex で最初の数字ではない文字のインデックスを取るとこんな感じ。
findIndex 関数では全て数字だった場合に KeyNotFoundException が上がるので、tryFindIndex 関数で option を返すようにした。

「C#で学ぶF#入門」復習メモ

C#で学ぶF#入門・再(1)に行ってきた。
参加前にひと通りテキストを読んで疑問だった点について当日教えていただいたのでメモ。
(テキストはこちらで公開されている)

 
open System
let a = 1
Console.WriteLine(a)
a = 2
Console.WriteLine(a)

これでaの値が2に変更されないのは分かるが、
「a = 2」でなぜ警告だけでコンパイルエラーにならないのか、
混乱の元ではないかと思っていたが、ここでの = 演算子は == と同じらしい。
実際に書いてみたら、確かに警告の内容も値を変更できないよ、
ではなくてboolを返しているけどそれを使ってないよ、だった。

 

練習問題で

printfn "%f" sqrt(2)

はエラーになるため

printfn "%f" (sqrt(2.))

にしないといけないというものについて、
カッコをつけようがつけまいが sqrt(2.) の評価順は変わらないのでは、と思っていたが、
カッコがない場合 printfn が 3つの引数「"%f"」「sqrt」「(2.)」を取ると解釈されるためだった。

 

演習問題のおさらい。
【問4】C# の while, for, break を使ったプログラムを再帰を使った F# で書きなおしてみるというもの。

  • F# にはオフサイドルールがある
  • 不等号には != ではなく <> を使用する
  • 配列に添字をつけて要素を取り出すには list[0] ではなく list.[0] とドットをつける

など教わりつつ、最終的にこんな形になった。
解答例では loop の中に getNumIndex を定義する形だった、
なるほどこの中でしか使わない場合はそちらのほうがよさそうだ。

 

C# との比較に重点を置いた F# の入門勉強会とのことで、
C# 自体あまり詳しくないため心許なさも感じていたが、
丁寧に初歩から教えていただき、また仕事で Scala を使っていることを話すと
ところどころ Scala の話も交えつつで
楽しい勉強会だった。
ありがとうございました。

MacBook Air で F#

C#で学ぶF#入門・再(1)に参加するにあたり、手持ちの MacBook Air で F# 開発環境を整えた作業メモ。

Microsoft Download Centerから fsharp.zip をダウンロードして適当な場所に解凍。

mono {F# HOME}/bin/fsi.exe --gui-

で対話環境が立ち上がることを確認。

次にMonoDevelopで動かしてみる。
[アドインマネージャ]→[Gallery]→[Language bindings] から
F# Language Binding を選択してインストールすると
[新しいソリューション]から F# 3.0 Console Project などを選択できるようになる。
が、実行できるようにするためには色々設定が必要っぽい。

まず F# 3.0 Console Project を新規に作成してビルドしようとすると
フレームワーク '.NETFramework 4.5'がインストールされていません。」
というビルドエラーが発生する。
デフォルトで作成される Program.fs のコメント

// NOTE: If warnings appear, you may need to retarget this project to .NET 4.0. Show the Solution
// Pad, right-click on the project node, choose 'Options --> Build --> General' and change the target
// framework to .NET 4.0 or .NET 4.5.

に従って、プロジェクトを右クリック→[オプション]→[ビルド]→[一般]から
Target Framework を Mono / .NET 4.0 に変更。

再度ビルドしようとすると
「アセンブリ 'FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' が見つかりません。」
が発生するので、参照を右クリック→[参照アセンブリの編集]から
FSharp.Core.dll(4.0.0.0) にチェックを入れて、既存の FSharp.Core(4.3.0.0) は除去する。
これでビルドまではできるようになった。

実行するとビルドエラーは発生しなくなるものの、
MonoDevelop.MacInterop.AppleScriptException: Exception of type 'MonoDevelop.MacInterop.AppleScriptException' was thrown.」
が発生して実行に失敗する。
解決方法が分からず困っていたらF#入門主催の @7shi さんに Twitter で教えていただいた。
プロジェクトを右クリック→[実行]→[一般]から[外部コンソールで実行]のチェックを外す。
これでMonoDevelop上での実行もできるようになった。

Scala で 可変リストへの要素追加を「::」「:::」での結合に置き換える

本エントリは Scala で ATND API を叩くScala で partition 関数を使用してリストの要素を振り分ける の続きです。

さて、ここまでくるともうひとつの ListBuffer 使用箇所も不変リストにしたい。したほうがいいはずだ、多分。
残った ListBuffer 使用箇所は、「"keyword=aaa,bbb", "ym=201207", "ymd=20120723"」といった要素を持つリストを用意して
mkString("&") で「"keyword=aaa,bbb&ym=201207&ymd=20120723"」といった文字列を作っている。
(話はそれるけど、要素の区切り文字や接頭辞・接尾辞をつけた文字列を作れる mkString って便利)

「::」で要素を連結したリストを作れば上手くいくのでは、と思い付き、以下のようにやってみた。

import scala.io.Source
import scala.xml.XML

import java.text.SimpleDateFormat

val source = Source.fromURL("http://api.atnd.org/events/?" + getParams(args))
val response = XML.loadString(source.mkString)

val orgFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val showFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm")

response \\ "event" foreach { e =>
    println("『%s』\n  %s\n  %s\n" format(
    e \\ "title" text,
	showFormat.format(orgFormat.parse(e \\ "started_at" text)),
	e \\ "place" text))
}

def getParams(args:Array[String]):String = {
	val ymPattern = """^\d{6}$""".r
	val ymdPattern = """^\d{8}$""".r
	
	val (ymList, otherList) = args partition (ymPattern.findFirstIn(_) != None)
	val (ymdList, keywordList) = otherList partition (ymdPattern.findFirstIn(_) != None)
	
	val paramsList =
 		createSearchQuery("keyword", keywordList) ::
 		createSearchQuery("ym", ymList) ::
 		createSearchQuery("ymd", ymdList) ::
 		List()
	
	paramsList.mkString("&")
}

def createSearchQuery(key:String, params:Array[String]):String = {
	if (!params.isEmpty) {
		key + "=" + params.mkString(",")
	} else {
		""
	}
}

「::」は要素となる型を先頭に追加したリストを作るので、createSearchQuery で要素が空でなければキーと値を文字列にして、
空であれば空文字を返すようにした。
一見うまくいったっぽいけど、空文字はリストの要素なので、例えば何も引数を渡さないと getParams が「&&」という文字列を返してしまう。
(空文字3つのリストなので、空文字・&・空文字・&・空文字が返る)
期待する動作は「&&」ではなくて空文字を作ってほしいので、別の方法を考える。

「:::」で連結してみよう。

import scala.io.Source
import scala.xml.XML

import java.text.ParseException;
import java.text.SimpleDateFormat

val source = Source.fromURL("http://api.atnd.org/events/?" + getParams(args))
val response = XML.loadString(source.mkString)

val orgFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val showFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm")

response \\ "event" foreach { e =>
    println("『%s』\n  %s\n  %s\n" format(
	e \\ "title" text,
	showFormat.format(orgFormat.parse(e \\ "started_at" text)),
	e \\ "place" text))
}

def getParams(args:Array[String]):String = {
	val ymFormat = new SimpleDateFormat("yyyyMM")
	ymFormat.setLenient(false)
	val ymdFormat = new SimpleDateFormat("yyyyMMdd")
	ymdFormat.setLenient(false)
	
	val (ymList, otherList) = args partition (validDateFormat(_, ymFormat))
	val (ymdList, keywordList) = otherList partition (validDateFormat(_, ymdFormat))
	
	val paramsList =
 		createSearchQuery("keyword", keywordList) :::
 		createSearchQuery("ym", ymList) :::
 		createSearchQuery("ymd", ymdList)
	
	paramsList.mkString("&")
}

def validDateFormat(arg:String, fmt:SimpleDateFormat):Boolean = {
	try {
		fmt.parse(arg)
		true
	} catch {
		case _:ParseException => false
	}
}

def createSearchQuery(key:String, params:Array[String]):List[String] = {
	if (!params.isEmpty) {
		List(key + "=" + params.mkString(","))
	} else {
		List()
	}
}

「:::」はリスト同士を連結して新しいリストを作る。
createSearchQuery で元の要素が何もなければ空リストを返すようにした。
これで何も引数を渡さないと getParams が空文字を返してくれるようになった。
(本題と関係ないけど、日付かどうかの判定を正規表現から
SimpleDateFormat で厳密にパースできるかどうかで判定する validDateFormat を作成した)

可変リスト使用箇所がなくなり、前より更に良くなった気がする!

Scala で partition 関数を使用してリストの要素を振り分ける

前回のエントリで、複数の引数を受け取って、数値6個ならymパラメータ、数値8個ならymdパラメータ、
それ以外ならkeywordパラメータとして検索条件に指定するために
可変リストをあらかじめ3つ用意してパターンマッチで各要素をそれぞれのリストに追加していくようにしていたが、
partition という関数TraversableLikeトレイトのメソッドというのが正しいのかな?)を使うと
上手く振り分けられそうだったのでやってみた。

partition は「リストの要素の型を引数に取って真偽値を返す関数」を引数に取って
条件に合う要素を集めたリストと合わない要素を集めたリストのタプルを返す。
また、partition に渡す関数にはRegexクラスの findFirstIn メソッドを使用したが、
これは文字列を取って Option[String] を返す。
Option 型は Haskell でいう Maybe みたいなもので、正規表現にマッチしない場合は None を返すようだ。

import scala.collection.mutable.ListBuffer
import scala.io.Source
import scala.xml.XML

import java.text.SimpleDateFormat

val source = Source.fromURL("http://api.atnd.org/events/?" + getParams(args))
val response = XML.loadString(source.mkString)

val orgFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val showFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm")

response \\ "event" foreach { e =>
    println("『%s』\n  %s\n  %s\n" format(
	e \\ "title" text,
	showFormat.format(orgFormat.parse(e \\ "started_at" text)),
	e \\ "place" text))
}

def getParams(args:Array[String]):String = {
	val ymPattern = """^\d{6}$""".r
	val ymdPattern = """^\d{8}$""".r
	
	val (ymList, otherList) = args partition (ymPattern.findFirstIn(_) != None)
	val (ymdList, keywordList) = otherList partition (ymdPattern.findFirstIn(_) != None)
	
	val paramsList = new ListBuffer[String]
	if (!keywordList.isEmpty) {
		paramsList += "keyword=" + keywordList.mkString(",")
	}
	if (!ymList.isEmpty) {
		paramsList += "ym=" + ymList.mkString(",")
	}
	if (!ymdList.isEmpty) {
		paramsList += "ymd=" + ymdList.mkString(",")
	}
	
	paramsList.mkString("&")
}

こんな感じになった。
書き直したのは getParams の中のみ。
正規表現にマッチした場合 Some(String) が、マッチしない場合 None が返るので、
正規表現にマッチしたかどうかを None かどうかで振り分けている。

可変リストの多用が少しは解消され、前よりは良くなったように思う。
ただ、partition を2回行なっているのはもうちょっと何とかできる気がするなあ…。

Scala で ATND API を叩く

Scala 勉強中なのでXML操作やパターンマッチングなどをATND APIを使って試してみた。

キーワード・イベント開催年月・イベント開催年月日を検索条件にすることができるようにして、
返ってきたXMLからタイトル、開始日時、開催場所を標準出力するようにしてみた。
引数を複数渡して、yyyyMM形式ならymパラメータに、yyyyMMdd形式ならymdパラメータに、 どちらにもマッチしないならkeywordパラメータに渡すことにした。
(数値が6個か、8個かでパターンマッチングしているので厳密には正しい日付かどうかチェックできていないけど)

で、試行錯誤の結果、こんな感じ。

import scala.collection.mutable.ListBuffer
import scala.io.Source
import scala.xml.XML

import java.text.SimpleDateFormat

val source = Source.fromURL("http://api.atnd.org/events/?" + getParams(args))
val response = XML.loadString(source.mkString)

val orgFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val showFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm")

response \\ "event" foreach { e =>
    println("『%s』\n  %s\n  %s\n" format(
	e \\ "title" text,
	showFormat.format(orgFormat.parse(e \\ "started_at" text)),
	e \\ "place" text))
}

def getParams(args:Array[String]):String = {
	val keywordList = new ListBuffer[String]
	val ymList = new ListBuffer[String]
	val ymdList = new ListBuffer[String]
	
	val ymPattern = """^(\d{6})$""".r
	val ymdPattern = """^(\d{8})$""".r
	
	args foreach { arg =>
		arg match {
			case ymPattern(ym) => ymList += ym
			case ymdPattern(ymd) => ymdList += ymd
			case _ => keywordList += arg
		}
	}
	
	val paramsList = new ListBuffer[String]
	if (!keywordList.isEmpty) {
		paramsList += "keyword=" + keywordList.mkString(",")
	}
	if (!ymList.isEmpty) {
		paramsList += "ym=" + ymList.mkString(",")
	}
	if (!ymdList.isEmpty) {
		paramsList += "ymd=" + ymdList.mkString(",")
	}
	
	paramsList.mkString("&")
}

…うーん、可変リストを多用している時点であんまりイケてないように思う。
リスト内包表記でymPatternにマッチするもの、ymdPatternにマッチするもの、
どちらにもマッチしないものをそれぞれ取り出すようにすることも考えたが、
そうするとリストを3回回すことになるのでそれもイマイチ。

一応期待通りに動いてはいるけど、まだまだ改善できそうな気がするなあ。

【追記】partition を使用した改良版を書きました。

Jython-bugs issue1642 に対するワークアラウンド

前回の補足。
[Jython-bugs] [issue1642] Proxy jsr223 Nullpointer no arguments の回避策を考える。
綺麗な方法は思いつかなかったと書いたけど、ラッパークラスでそこそこ見栄え良くなるかも。

まず、大前提として目的の再確認。

  • 呼び出し側の Java は引数と戻り値の型を知っていたいので、インターフェースを Java 側で切る
  • スクリプト言語でそれを実装するが、どの言語でどのように実装しているかは呼び出し側は意識する必要は無い
  • 上記二つを満たしつつ、Jython の NullPointerException 問題に対処したい

つまり、NullPointerException が飛んだらどうこう、というのは呼び出し側では意識したくない。
また、必ず引数を渡すようにインターフェースを変更すると
スクリプト言語実装(今回の場合、変更の必要の無い JavaScriptRuby)も全て変更しないといけなくなるので却下。

そこで、ラッパークラス案。
まず、インターフェースに引数ありの getName() を追加。
通常使う必要は無いので @Deprecated アノテーションをつけておく。

次に、ラッパークラスを追加する。
getName() で NullPointerException が飛んだときのみ引数ありの方を呼ぶ。

これだけでは getName(String) を呼んだときに
「javax.script.ScriptException: TypeError: getName() takes no arguments (1 given)」
が発生してしまうため、Python 側の実装も変更する。

準備OK。ラッパーでくるんで返してやろう。

public static void main(String[] args) throws Throwable {
  ScriptExecutor executor = new ScriptExecutor();
  executor.execute("javascript", "serviceImpl.js");
  executor.execute("python", "serviceImpl.py");
  executor.execute("ruby", "serviceImpl.rb");
}
public void execute(String shortName, String filePath) throws Throwable {
  try {
    ScriptService service = getService(shortName, filePath);
    service.echo("hello!");
    System.out.println(service.getName());
    System.out.println(service.calculate(10));
    
  } catch (UndeclaredThrowableException e) {
    if (e.getUndeclaredThrowable() != null) {
      throw e.getUndeclaredThrowable();
    }
    throw e;
  }
}
private ScriptService getService(String shortName, String filePath)
    throws FileNotFoundException, ScriptException {
  ScriptEngineManager manager = new ScriptEngineManager();
  ScriptEngine engine = manager.getEngineByName(shortName);
  engine.eval(new FileReader(filePath));
  return new ScriptServiceWrapper(((Invocable) engine).getInterface(ScriptService.class));
}

引数の無いメソッド全てに使いもしない引数ありメソッドを
インターフェース側で用意しないといけないけれど、
呼び出し側や JavaScript, Ruby の実装を変える必要が無く、
NullPointerException の発生を意識するのがラッパーのみに閉じられるので
まずまず許せる回避策なんじゃないかな。