LL脳がscalaの勉強を始めたよ その21


Scalaコップ本の第8章に進みますよー、8章は関数とクロージャですよー

とりあえず今回は関数に関するアレやコレについて色々やっていきますよ

メソッド

関数定義で一番よくあるのはオブジェクトのメンバーとしてのメソッドですねっていうことで、とりあえずサンプルとしてファイル内の特定の文字数以上の行を抜き出す処理を書いてみますよー

サンプル処理の概要は次のとおり

  • 処理対象のファイル名を引数として指定する
  • 抜き出し条件となる一行あたりの文字数を引数として指定する
  • 条件にマッチした行を"ファイル名: 行内容"の形式で画面上に出力する

んで、こんな感じになりますよ

// ioパッケージを読み込み
import scala.io.Source
// オブジェクトの定義(これはスタンドアロンシングルトンオブジェクトかな?)
object LongLines{
    // ファイルを処理しますよー
    def processFile(filename:String, width:Int){
        val source = Source.fromFile(filename)
        // ファイルの行ごとにループ処理します(processLineの呼び出し)
        for(line <- source.getLines)
            processLine(filename, width, line)
    }
    // 指定された行が指定以上の長さかどうか判定して表示する処理
    // 外部からは遣わせないのでprivate
    private def processLine(filename:String,
        width:Int, line:String){
        if(line.length > width)
            // 条件に合えば"ファイル名: 行内容"の形式で表示しますよ 
            println(filename + ":" + line.trim)
    }
}

こいつをLongLines.scalaとして保存しますー

オブジェクトのスクリプト実行を復習

さて、LongLinesオブジェクトを実行するためにスクリプト実行したいので4章の内容に従ってmain処理用のスタンドアロンシングルトンオブジェクトをFindLongLines.scalaとして保存しますよ

object FindLongLines {
    // スクリプトで実行する内容をmainに書きますよー
    def main(args:Array[String]){
        // コマンド引数取得ですね
        val width = args(0).toInt
        // 第1引数は行の長さ条件なのでdropしときますね
        for(arg <- args.drop(1))
            // 第2引数以降に渡されたファイルを処理(複数可)
            LongLines.processFile(arg, width)
    }
}

コンパイルしないと使えないのでコンパイルしますー

$ scalac FindLongLines.scala LongLines.scala 

実行しますよー

$ scala FindLongLines 45 LongLines.scala

// 実行結果ですよー
LongLines.scala:def processFile(filename:String, width:Int){
LongLines.scala:val source = Source.fromFile(filename)
LongLines.scala:// ファイルの行ごとにループ処理します(processLineの呼び出し)
LongLines.scala:processLine(filename, width, line)
LongLines.scala:println(filename + ":" + line.trim)

コップ本の実行結果と若干違うのはご愛嬌。Scala標準のTab文字(スペース2文字)をガン無視して4文字スペースTabで書いている&コメントが付いているからですね(´・ω・`)

でもちょっとScalaっぽくないよね…だってさ

上記サンプルは、処理的は書けているんだけどもScala的にごちゃごちゃしているのでドンドン書き換えていきますよー

ローカル関数

ローカル関数使いマス

Scalaでは関数の中に関数をもつことができるのですが、こいつをローカル変数と呼ぶみたいですね。JavaScriptなんかではよくやりましたねぇ。

さて、上で書いたLongLinesオブジェクトは2つの関数を持っているんだけども、ぶっちゃけprivateなprocessLineはprocessFileからしか使わないのでローカル関数にまとめちゃおうぜ、ってことでやってみます。

// processFileにまとめますよー
def processFile(filename:String, width:Int){
    // processLineをローカル関数として配置しますよー
    def processLine(filename:String, width:Int, line:String){
        if(line.length > width)
            print(filename + ": " + line)
    }
    // ここからはprocessFileの処理ですよ
    val source = Source.fromFile(filename)
    for(line <- source.getLines){
        processLine(filename, width, line)
    }
}

こんなふうに特定の関数からしか使わないヘルパー関数をローカル関数としてまとめていくことで、外部から見える部分に大量の使わないメソッドが溢れてしまうという問題を防げそうですな。

ローカル関数から外側の変数にアクセスしますよ

さて、上のようにローカル変数を使うことでメソッドをまとめてコンパクトにすることができたわけだけども、各関数の引数が重複しまくって気持ち悪いのです。

そこで、ローカル関数は外側の関数の変数にアクセス出来るという性質を利用してすっきりさせてやりましょー

import scala.io.Source
object LongLines {
    def processFile(filename:String, width:Int){
        // processLine内でprocessFileのfilenameとwidthにアクセスできるので
        // 引数から外してしまうのです
        def processLine(line:String){
            if(line.length > width)
                print(filename + ": " + line)
        }
        val source = Source.fromFile(filename)
        for(line <- source.getLines)
            processLine(line)
        }
    }
}

うん、これでかなりすっきりしましたな。こんなふうにまとめて書けると”まとめたがり人(架空の生き物)”としてはかなり幸せですね(`・ω・´)

一人前の存在としての関数

なんだか妙な名称ですが、関数リテラルとか関数リテラルの値渡しとかそんな感じのものっぽいですねー

言葉だけだと混乱しそうなので手を動かしますよー

関数リテラルの復習

関数リテラルの形式はこんなでしたな

(変数:型, 変数:型) => 処理

具体的なものを書いてみるとこうなりますね

(x:int) => x + 1

これは任意のxをx+1という処理にマッピングする関数という表現みたいです

関数を変数に格納しようぜ

さて、Scalaでは関数リテラルリテラルなので変数に格納できますねー、ますますJavaScriptが懐かしくなりますねー

とりあえず変数に格納してみますかねー

// 上記の関数リテラルを変数increaseに格納しました
scala> var increase = (x:Int) => x+1
increase: (Int) => Int = <function>
// increaseの中身は関数ですねー
scala> increase
res3: (Int) => Int = <function>

// 実行してみますよー
scala> increase(10)
res4: Int = 11

// var定義なので書き換えもOKですな
scala> increase = (x:Int) => x + 10
increase: (Int) => Int = <function>
// 実行しましたよ
scala> increase(10)
res6: Int = 20

あ、関数リテラルは中カッコ{}をつかうことで複数行にも対応可デス

// 複数行定義してみますよ
val hello = (x: Int) => {
   // 引数に応じてしつこいくらいHelloを叫びますよ
   println("Hello " * x)
   println("World!")
}
// 実行結果ですよー
scala> hello(3)
Hello Hello Hello 
World!
foreachの引数としての関数リテラル

Scalaのコレクション(ListとかSetとかのアレ)はすべからくforeachメソッドを持っているのだけども、その引数として関数リテラルを指定して処理させることができまっせ…という内容です。

とりあえずイメージをつかむためにサンプルをー

// リストを定義します
scala> val l = List(1,2,3,4,5,6)
l: List[Int] = List(1, 2, 3, 4, 5, 6)
// リストのforeachメソッドとして各アイテムを2倍する関数リテラルを指定しますよ
// xに各アイテムが入りますね
scala> l.foreach((x:Int) => println(x * 2))
2
4
6
8
10
12

うん、サンプルがすべてを語るような感じですねー

ちなみにforeachメソッドはList, Set, Array, Mapに共通するスーパートレイトIterableで定義されてるそうです。詳しくは17章みたいですね。ついでにトレイトについては12章でやるんでしたっけ?

filterの引数としての関数リテラル

foreachと同様にコレクションのfilterメソッドの引数にも関数リテラルが使えますよー、とのことなので使ってみます。ちなみにfilterメソッドを使うとコレクションの要素から条件に合うものだけ取り出せますねー

// リストを定義します
scala> val l = List(1,2,3,4,5,6)           
l: List[Int] = List(1, 2, 3, 4, 5, 6)
// 偶数だけ取り出してみますよ
scala> l.filter((x:Int) => { x % 2  == 0 })
res16: List[Int] = List(2, 4, 6)

foreachやfilterの関数リテラル利用は慣れるとメチャメチャ楽しそう、多分Pythonのリスト内包表現へのあこがれですかねー

foreachやfilterについては16章やら17章で詳しくやるみたいですね

関数リテラルの短縮形

関数リテラルの形式も実際は省略できますよーという悪魔の囁きです

まずは型から省略しますか

引数の型を省略できますよ

// リスト定義デス
scala> val l = List(1,2,3,4,5,6)           
l: List[Int] = List(1, 2, 3, 4, 5, 6)
// 引数の型を省略しました
scala> l.filter((x) => { x % 2 == 0 })
res19: List[Int] = List(2, 4, 6)

関数リテラルの引数の型は処理対象のコレクションから推測できるので省略してもダイジョブみたいです。基本的に省略しておいてコンパイラに怒られたときに付与するくらいでOKとのことです。

ちなみにこういった処理の対象からの型推論をターゲットによる型付けと呼ぶみたいですねー


まったく関係ないけども型付けを何度も片付けと変換して苦い思いをしておりマス

ついでに()も省略しようぜ

推論で型が分かる場合は引数の()も省略できますよ

scala> l.filter(x => { x % 2 == 0 })  
res20: List[Int] = List(2, 4, 6)
せっかくだから完全体にしてしまえ

そもそも一行の式なので{}すらいらないよね、ということで簡易化しますよ

scala> l.filter(x => x % 2 == 0 )   
res21: List[Int] = List(2, 4, 6)

できまったー

ちょっと疑問

---- <あとから書いた> ----

以下、普通にできました。何を勘違いしてたんだろう(´・ω・`) ... どうもfilterみたいな使い方をしようと斜めに突っ走ってたみたいです。

scala> val h = (a:Int, b:Int) => a * b
h: (Int, Int) => Int = <function>

scala> h(1,3)
res0: Int = 3

対話コンソールで単独実行だとダメなのかしらね…引数1個だと上手くいくんだけどな


そんなわけで以下の内容は多分解決済みです
---- ----
引数が2つ以上ある関数リテラルって定義できるのかしら?

試してみるとダメっぽいんだけど、そういうもん?

scala> (a:Int, b:int) => a * b
<console>:1: error: not a legal formal parameter
       (a:Int, b:int) => a * b

いじょうー

とりあえず関数リテラルの基本をやってみましたよ、次回はプレースホルダーあたりですかね。クロージャまで行ければ…無理だろうな…