yoshikit1996’s diary

日々勉強したことの備忘録です。

トレイトのwithとextendsの使い分け

withとextendsの使い分けがややこしいと思うのでメモ。

文法的な使いわけ

  • クラス定義時にミックスインする場合、extendsした後にwithを使う。
  • インスタンス生成時にトレイトをミックスインする場合、withを使う。
object Main extends App{
    // クラス定義時にトレイトをミックスイン
    class MyQueue extends BasicIntQueue with Incrimenting
    // インスタンス生成時にトレイトをミックスイン
    val queue = (new BasicIntQueue with Incrimenting with Filtering)    
}

ハマったところ

インスタンス生成時にトレイトをミックスした場合の挙動」と「クラス定義時にミックスインした場合の挙動」は
同じだと思っていたら、ハマりました。実際は異なるみたいです。

下記に例を示します。

object Main extends App{
    // インスタンス生成時にトレイトをミックスインする場合
    {
        case class MyString(value: String){
            override def toString = value
        }
        
        trait Hoge{
            override def toString = super.toString + "hoge"
        }
        
        trait Fuga{
            override def toString = super.toString + "fuga"
        }
        
        val myString = new MyString("foobar") with Hoge with Fuga
        
        println(myString) // foobarhogefuga
    }
    
    // クラス定義時にトレイトをミックスインする場合
    {
        case class MyString(value: String) extends Hoge with Fuga{
            override def toString = value
        }
        
        trait Hoge{
            override def toString = super.toString + "hoge"
        }
        
        trait Fuga{
            override def toString = super.toString + "fuga"
        }
        
        println(MyString("foobar")) // foobar
    }
}

両方とも「foobarhogefuga」になるかと思いきや、クラス定義時にトレイトをミックスインすると「foobar」になりました。

「foobar」になる理由はおそらく↓こうだと思います。

  1. HogeにFugaがミックスインされる
  2. Hoge#toStringをMyString#toStringがオーバーライドしてしまう
  3. 結果、MyString#toStringがvalueを返して、foobarになる

ハマった理由

「withとextendsは、使い方が異なるだけで、同じもの」と思っていました。
また、「線形化」という言葉とwithとextendsの使い分けを理解していなかったみたいです。

誤った理解

extendsの前にくるのはミックスインの対象。
extendsおよびwithの後ろにくるトレイトはミックスインの対象に対して線形化される。

ミックスインの対象 extends trait1 with trait2 with ...
正しい理解

withの前にくるのはミックスインの対象。
withの後ろにくるトレイトはミックスインする対象に対して線形化される。

ミックスインの対象 with trait1 with trait2 with ...

trait1がミックスインの対象、trait2以降のトレイトはtrait1に対して線形化される。

クラス extends trait1 with trait2 with ...

Scalaでクイックソート

Scalaで3通りのクイックソートを実装してみました。

object QuickSort extends App {

  val nums = List(32, 23, 10, 1, -100, 3, 999)

  {
    // 9行: ふつうの実装
    def quickSort(nums: List[Int]): List[Int] = {
      if (nums.isEmpty)
        List()
      else {
        val left = nums.filter(_ < nums.head)
        val right = nums.filter(nums.head < _)
        quickSort(left) ++ List(nums.head) ++ quickSort(right)
      }
    }

    println(quickSort(nums))
  }

  {
    // 7行: matchを使った実装
    def quickSort(nums: List[Int]): List[Int] = nums match {
      case List() => List()
      case head :: tail =>
        val left = tail.filter(_ < head)
        val right = tail.filter(head < _)
        quickSort(left) ++ List(head) ++ quickSort(right)
    }

    println(quickSort(nums))
  }

  {
    // 7行: foldを使った実装
    def quickSort(nums: List[Int]): List[Int] = {
      nums.headOption.fold(List[Int]()){ head =>
        val left = nums.filter(_ < head)
        val right = nums.filter(head < _)
        quickSort(left) ++ List(head) ++ quickSort(right)
      }
    }

    println(quickSort(nums))
  }
}

ifやmatchを使った実装は、比較的わかりやすいと思います。
foldを使った実装は、関数型っぽい実装だと思います。

Scalaアンチパターン

過去に自分が書いたScalaコードのアンチパターンをまとめました。

定数名が大文字

Scalaでは定数名はパスカルケースで書きます。

// ダメ
val MAX = 100
val MIN = 1
val HOGE_HOGE = "hogehoge"

// 良い
val Max = 100
val Min = 1
val HogeHoge = "hogehoge"

map + getOrElse

map(???).getOrElse(???)は冗長です。foldを使ってシンプルに記述することができます。

// ダメ
object Main extends App{
    val option = Some("hoge")
    val str = option.map(_.toUpperCase).getOrElse("")
    
    println(str)
}
// 良い
object Main extends App{
    val option = Some("hoge")
    val str = option.fold("")(_.toUpperCase)
    
    println(str)
}

returnを使う

returnはScalaらしくないので使うべきではありません。
Scalaの主な構成要素は式です。return文は異物感がします。

object Main extends App{
    
    def hoge(isHogeHoge: Boolean): String = {
        if(isHogeHoge) return "hogehoge"
        return ""
    }
    
    println(hoge(true))
}

get

option.getとすると例外が発生する可能性があります。getOrElseやmapで代用すべきです。

1文字の変数名"l"

Either型の値に対してmatchを使う場合、Leftにマッチさせる変数名に"l"(エル)を使いたくなる場合があると思います。
しかし、"I"(アイ)や"1"(イチ)と区別しづらくなるのでダメだと思います。

either match {
  case r: Right[???, ???] => ???
  case l: Left[???, ???] => ???
}

他にも、0(ゼロ)とO(オー)も識別しずらいと思います。スコープが短いと、使いたくなるのですが。。。

Either

Eitherはモナドでないので使いづらいです。
EitherよりOptionを使うべきです。

publicメソッドに戻り値の型を書かない

publicメソッドは外部のクラスから使われるので戻り値の型は書くべきです。

// ダメ
def hoge(fuga: String) = ???

// 良い
def hoge(fuga: String): String = ???

冗長な型アノテーション

// ダメ
val x: HashMap[Int, String] = new HashMap[Int, String]()
// 良い
val x = new HashMap[Int, String]()
val x: Map[Int, String] = new HashMap()

コップ本には、次のようなことが書かれています。

一般にドキュメントが役に立つのは、簡単にはわからないことを示しているときだ。

型が簡単にはわからないときに、型アノテーションをつけるべきです。

末尾再帰に@tailrecアノテーションをつけない

@tailrecアノテーションを使うと一目で末尾再帰を使っていることがわかります。

タプルで_1とか_2を使う

tuple._1とtuple._2とすると、意味がわかりにくいです。パターンマッチを使った方が意味がわかりやすいと思います。

object Main extends App{
    val prefectureCaptals = List(
        ("滋賀", "大津"),
        ("三重", "津"),
        ("兵庫", "神戸")
    )
    
    // ダメ
    prefectureCaptals.map( pc =>
        println(pc._1 + " : " + pc._2)
    )
    
    // 良い
    prefectureCaptals.map(_ match {
        case (prefecture, captal) => println(prefecture + " : " +captal)
    })
    
    // 良い
    val (prefecture, capital) = prefectureCaptals.head
    println(prefecture + " : " + capital)
    
    // 良い
    for((prefecture, capital) <- prefectureCaptals){
        println(prefecture + " : " + capital)
    }
}

foldLeftよりreduceLeft

初期値が不要な場合はfoldLeftではなく、reduceLeftを使うべきです。

object Main extends App{
    val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    // ダメ
    val sum1 = list.foldLeft(0){_ + _}
    // 良い
    val sum2 = list.reduceLeft{_ + _}
}

IdeaでSBTプロジェクトをシンタックスハイライトする

plugins.sbtに↓を付け加えて、gen-ideaコマンドを打てば完了。

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
sbt gen-idea

GitHub - mpeltonen/sbt-idea: A simple-build-tool (sbt) plugin/processor for creating IntelliJ IDEA project files

IntelliJ IDEA Scala plugin's syntax highlighting displays Scala packages in red - Stack Overflow

Javaのstaticメソッドに相当するもの

Javaのstaticメソッドは、Scalaのコンパニオンオブジェクトのメソッドに相当すると思う。

Javaでstaticメソッドを使う場合、こんな↓感じだが、

public class Main {
    public static void main(String[] args) throws Exception {
        for(int i = 0; i < 10; i++){
            new User("hoge");
            System.out.println(User.count);
        }
    }
}

class User {
    public static int count = 0;
    private String name;
    public User(String name){
        count += 1;
        this.name = name;
    }
}

Scalaだと、こんな↓感じになる。

    case class User(val name: String)
    object User {
        var count = 0
        def apply(name: String) = {
            count = count + 1
            new User(name)
        }
    }
    
    for(i <- 0 to 10){
        User("hoge")
        println(User.count)
    }

case classの性質

case classには、下記のような性質があります。

  • ボイラープレートが減る

apply, unapply, equals, canEqual, hashCode, toString, copyメソッドが自動生成される。

  • 不変データを扱うのに適している

コンストラクタのパラメータが勝手にvalになる。

クラスを作成するときは、なるべくcase classを使った方が良いプログラムをかけそうです。

カリー化と部分適用の関係

カリー化

カーリー化とは、

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
カリー化 - Wikipedia

なので、↓のコードのf関数や

// ソースコード1
object Main extends App{
    val add = (x: Int, y: Int) => x + y
    val f = (x: Int) => {
        (y: Int) => add(x, y) // add関数が部分適用されている。
    }
    println(f(1)(2))
}

↓のコードのadd関数は、カリーされた関数ということになります。

// ソースコード2
object Main extends App{
    val add = (x: Int) => (y: Int) => x + y
    println(add(1)(2))
}

部分適用

部分適用とは、引数の一部を関数に渡すことです。
ソースコード1のf関数で部分適用が行われています。

結論

部分適用は、カリー化するため手段のうちの1つです。