人のコード勝手にいじるシリーズ

この前のエントリの続き


エラー修正して、とりあえず実行できるようになったところで、まずは・・・classとobjectに分かれてるけど、この場合objectだけでいいんじゃね?って思ったので*1objectだけにしちゃいました

object Reflexa {
  
    val url = "http://labs.preferred.jp/reflexa/api.php"

    /*
     * reflexa apiへアクセス
     */
    def getXMLSource(query: String) = {
        val src = Source.fromURL(url + "?q=" + query + "&format=xml","UTF-8")
        println()
        XML.loadString(src.mkString)
    }

    /*
     * 検索結果のxmlをパースし、結果の配列を作成
     */
    def makeWordList(query: String) = {
        val xml = getXMLSource(query)
        val words = xml \\ "word"
        words size match {
            case 0 => Array()
            case _ => words.map(word => word.text).toArray
        }
    }

    /* 
     * 検索文字列からqueryを作成
     * 文字列同士は「%20」で連結
     */
    def makeQuery(search_words: Array[String]) = {
        search_words.map(URLEncoder.encode).mkString("%20")
    }
    
    def search(search_words: Array[String]) = {
        val query = makeQuery(search_words)
        makeWordList(query)
    }



    def main(arg: Array[String]):Unit = {
        val line = Console.readLine(">>> ")
        val split_reg = """[\s ]""".r
        val list = search(split_reg.split(line))

        list.map(println)
    }
 
}

classにするかどうかっていうのは、自分の判断基準としては、

  • 複数インスタンスを生成する必要がある(なおかつ複数生成した場合に、それぞれ違う機能や役割をもつ)
  • 生成したインスタンスが状態を持つ必要がある

という理由でしょうか?*2
逆に言えば、純粋な関数だけしかない場合は、objectでいいってことになります。

次に色々細かいところを"自分だったらこう書く"っていう判断基準で書き換えてみます。

/**
 * あえて、全部戻り値の型かいてみた
 */
object Reflexa {
  
    val url = "http://labs.preferred.jp/reflexa/api.php"

    /*
     * reflexa apiへアクセス
     */
    def getXMLSource(query: String):Elem = {
        //ゆろよろさんのblogにも書いてあったけど、ホントはSourceって閉じないといけない・・・
        val src = Source.fromURL(url + "?q=" + query + "&format=xml","UTF-8")
        println()
        val result = XML.loadString(src.mkString)
        src.close
        result
    }

    /*
     * 検索結果のxmlをパースし、結果の配列を作成
     */
    def makeWordList(query: String):Array[String] = {
        val xml = getXMLSource(query)
        val words = xml \\ "word"
        words.map(_.text).toArray  //0の場合の判断いらないはず。mapの引数の名前省略できる
    }

    /* 
     * 検索文字列からqueryを作成
     * 文字列同士は「%20」で連結
     */
    def makeQuery(search_words: Array[String]):String = {
        search_words.map(URLEncoder.encode).mkString("%20")
    }
    
    def search(search_words: Array[String]):Array[String] = {
        val query = makeQuery(search_words)
        makeWordList(query)
    }

    def main(arg: Array[String]):Unit = {
        val line = Console.readLine(">>> ")
        val split_reg = """[\s ]""".r
        val list = search(split_reg.split(line))

        list.foreach(println) //値を返さない(かえされたもの使わない)ので、mapではなくforeachを使うべき
    }
}

つぎは・・・勉強会でも話でたんですが、ArrayかListどっち使うかって言う話。
違いはかなり色々あって、時間あれば別エントリで書いてみますが、
個人的には、scalaや関数型になれるためには、基本的に何も考えずにList使ったほうがいいと思います。
サーバーサイドでずっと動き続けるような、重要なプログラムかくなら別ですが、小さいプログラムなら、パフォーマンスなんて考えても殆ど変わりません。

むしろ関数型に慣れるためには、Arrayじゃないとかけなそう(つまり値を途中で変更する必要がありそう)な場合でも、Listでかくにはどうすればいいんだろう?っていう姿勢でいたほうがいいです*3

ってことでListに書き換えます。この場合は何も考えずに書き換えればいいだけで、とくに難しくありませんが。
その他細かいとこ変更してみる

import scala.io.Source
import scala.xml.{XML,Elem}//まとめて1行に書くことできる
import java.net.URLEncoder

//Listはインポートする必要ない(Predefでimportされてる)

object Reflexa{
  
    val url = "http://labs.preferred.jp/reflexa/api.php"

    /** 
     * @param query エンコード済みの、検索する文字列
     * @return 検索結果のxml
     */
    def getXMLSource(query: String):Elem = {
        val src = Source.fromURL(url + "?q=" + query + "&format=xml","UTF-8")
        val result = XML.loadString(src.mkString)
        //ここにprintlnあるのは、おかしい気がするので移動
        src.close
        result
    }

    /**
     * @param query 検索用にエンコード済みの、検索する文字列
     * @return wordのタグの中身のList
     */
    def makeWordList(query: String):List[String] = {
        val xml = getXMLSource(query)
        val words = xml \\ "word"
        words.map(_.text).toList
    }

    /** 検索文字列からqueryを作成
     * @param 検索する文字列
     * @return それぞれの文字列をエンコードして、文字列同士を「%20」で連結したもの
     */
    def makeQuery(searchWords: List[String]):String = 
        searchWords.map(URLEncoder.encode).mkString("%20")
        
        //関数自体が1つの式の場合中括弧省略できる(省略すべきかどうかの判断基準はよくわからんが、標準ライブラリはけっこう省略してると思う)
    
        
    /**
     * @param searchWords 検索する文字列のList 
     * @return 検索結果の文字列のList
     */
    def search(searchWords: List[String]):List[String] = {
        val query = makeQuery(searchWords)//scalaでは変数名で単語と単語つなげるときはアンダースコア使わない慣習
        makeWordList(query)
    }
    
    def main(arg: Array[String]):Unit = {
        val line = Console.readLine(">>> ")
        val list = search(line.split(" ").toList)//この場合だったら正規表現使わなくても、これだけでもできる
        println//printlnはこっちにもってきた
        list.foreach(println)
    }
}

ここで気づいたのが、
"URLのエンコードって、空白を%20に変換するんじゃなかったっけ?だったら、コンソールからの入力まとめて全部エンコードすればよくね?"

っていうこと。
拡張性とか、入力文字列の検査とか考えると、むしろ今のままのほうがいい気がしますが、"こういう風にも書けるよ"って例ってことで、あえて書き換えてみます。

    val url = "http://labs.preferred.jp/reflexa/api.php"

    /** 
     * @param query エンコード済みの、検索する文字列
     * @return 検索結果のxml
     */
    def getXMLSource(query: String):Elem = {
        val src = Source.fromURL(url + "?q=" + query + "&format=xml","UTF-8")
        val result = XML.loadString(src.mkString)
        src.close
        result
    }

    /**
     * @param query 検索用にエンコード済みの、検索する文字列
     * @return wordのタグの中身のList
     */
    def makeWordList(query: String):List[String] = {
        val xml = getXMLSource(query)
        val words = xml \\ "word"
        words.map(_.text).toList
    }
      
    /**
     * @param searchWords 検索する文字列
     * @return 検索結果の文字列のList
     */
    def search(searchWords: String):List[String] = 
      makeWordList( URLEncoder.encode(searchWords) )
    
    
    def main(arg: Array[String]):Unit = {
        val line = Console.readLine(">>> ")
        val list = search(line)
        println
        list.foreach(println)
    }

こんな感じでしょうか。
やってみて思ったけど、このぐらい短いと、(細かいところは、色々違う書き方をすることもできますが)明らかに書き換えが必要そうなところってないですね。

プログラム拡張していくんだったら、
getXMLSourceっていうのが、URL全体を渡して、結果をxmlで返すようにしたほうが汎用性がある気もしますが、
このぐらいの短いコードなら、そんなことより直接埋め込んだほうが、分かりやすい気もしますし。

あと付け加えるなら、これはscalaに限らないと思いますが、コメントについて、
"関数の中の処理がどうなっているか?"
を書くのではなく、
"なにを渡すと、なにを返してくれるか?"
を書いたほうがいいと思います。
javaやってる人にはおなじみですが、
そのために、scaladoc形式で最低限 @param@return をかくといいかと思います。
あとは、privateのものはコメント省略しても、publicのものはちゃんとかくとか
*4

*1:拡張性とか考えると、classのほうがいい場合もありますが

*2:この2つは関連づいてますが

*3:この辺コップ本の受け売りですがw

*4:こんな小さいプログラムだったら、自分もコメント省略したりしますけど