본문 바로가기

Languages/Scala

[Scala Cats] Cats의 타입 클래스

반응형

이미지 출처 : Pixabay

지난번 포스트에서 타입 클래스를 스칼라에서 어떻게 구현하는지를 알아보았습니다.

 

[Scala Cats] 스칼라 타입 클래스 기초

귀여운 고양이로 시작합니다. 스칼라 Cats는 스칼라에서의 Functional한 프로그래밍을 위한 추상화를 제공하는 라이브러리입니다. typelevel/cats Lightweight, modular, and extensible library for functional..

sean-ma.tistory.com

이번 글에서는 타입 클래스가 스칼라 Cats 라이브러리에서 어떻게 구현되어 있는지를 알아보겠습니다.

레퍼런스는 Scala with Cats[무료 PDF] 입니다.

 

 


cats.Show

Cats는 어떤 클래스, 인스턴스 혹은 인터페이스 메소드를 사용할건지 정할 수 있도록 모듈화된 구조로 작성되었습니다. 그 중, cats.Show를 먼저 확인해 보겠습니다. Show는 개발자 친화적인 콘솔 아웃풋 함수로, toString을 사용할 필요가 없도록 하는 출력 매커니즘을 제공합니다.

package cats

trait Show[A] {
	def show(value: A): String
}

Cats의 타입 클래스는 cats 패키지 안에 정의되어 있습니다. Show 메소드는 패키지로부터 바로 import될 수 있으며, 모든 Cats의 타입 클래스의 컴패니언 오브젝트에는 사용자가 지정하는 인스턴스를 찾아 동작하는 apply 메소드가 정의되어 있습니다.

val showInt = Show.apply[Int]

하지만 위 코드는 동작하지 않습니다 - apply 메소드는 암시적 파라미터를 사용해 각각의 인스턴스를 찾기 때문에 해당 인스턴스를 묵시적 스코프 내로 가져와야 하기 때문입니다.

 

cats.instances 패키지에는 다양한 타입에 대한 기본 인스턴스들이 정의되어 있습니다.

Show 타입 클래스에 사용하기 위한 Int 와 String 인스턴스를 import해 봅니다.

import cats.instances.int._
import cats.instances.string._

val showInt: Show[Int] = Show.apply[Int]
val showString: Show[String] = Show.apply[String]

val intAsString: String = showInt.show(123)
val stringAsString: String = showString.show(123)

Cats는 각각의 타입 클래스에 대해 syntax를 제공하고 있는데, Interface Object를 생성해 사용하는 대신 Interface syntax를 사용하고자 한다면 cats.syntax.show._ 를 import 해 스코프 내에 있는 Show 인스턴스에 show 메소드를 추가합니다.

import cats.syntax.show._

val shownInt = 123.show
val shownString = "abc".show

대부분은 다음과 같은 import문으로 파일을 시작하는데, 네이밍 충돌(naming conflict)이나 모호한 암시성(ambiguous implicits) 문제가 발생하는 경우에는 구체적인 import를 사용합니다.

import cats._
import cats.implicits._

 

Show 타입 클래스를 커스텀 인스턴스 (예. 사용자가 만든 case class 등)와 사용하려면 주어진 타입에 대한 트레이트를 구현합니다.

final case class User(name: String, mail: String)

implicit val userShow: Show[User] = 
	new Show[User] {
		def show(user: User): String = s"name:${user.name}, mail:${user.mail}"
	}

또한 Cats는 이런 과정을 단순화하기 위해 코드블럭과 같이 Show의 컴페니언 오브젝트를 위한 두 개의 생성자 메소드를 제공하고 있어 직접 정의한 타입들의 인스턴스를 생성하는데 사용할 수 있습니다.

object Show {
	// Convert a functino to a `Show` instance:
	def show[A](f: A => String): Show[A] = ???
	
	// Create a `Show` instance from a `toString` method:
	def fromToString[A]: Show[A] = ???
}

위 생성자 메소드를 사용하여 다음과 같이 간단하게 타입 인스턴스를 생성할 수 있습니다.

implicit val userShow: Show[User] = Show.show(user => s"name:${user.name}, mail:${user.mail}")

 

cats.Eq

다음으로 알아볼 타입 클래스는 cats.Eq 입니다. Eq 타입 클래스는 type-safe한 동등성 체크 (equality check) 및 스칼라 내장 == 연산자의 문제를 해결하기 위해 디자인되었습니다.

스칼라 개발자들이 많이 하는 실수 중 하나인 다음 코드를 살펴보겠습니다.

List(1, 2, 3).map(Option(_)).filter(item => item == 1)

위 코드의 filter 절은 Int와 Option[Int]를 비교하기 때문에 항상 false를 리턴하게 됩니다. Eq는 이런 문제를 해결하기 위해 만들어졌습니다.

 

cats.syntax.eq에 정의된 Interface syntax는 스코프 내 인스턴스 Eq[A]에 대한 동등성 체크 메소드를 제공합니다.

interface syntax method Definition
=== 두 오브젝트에 대한 동등성 비교
=!= 두 오브젝트에 대한 비동등성 비교

 

몇 가지 예시를 살펴보기 위해 Eq 타입 클래스를 import하고 Int에 대한 인스턴스를 생성하고 사용해 보겠습니다.

import cats.Eq
import cats.instances.int._

val eqInt = Eq[Int]

eqInt.eqv(123, 123) //true
eqInt.eqv(123, 234) //false

스칼라의 == 메소드와 달리 Eq.eqv 는 서로 다른 타입에 대한 비교를 시도하면 오류를 출력합니다.

eqInt.eqv(123, "234") //error : type mismatch

또한 cats.syntax.eq 를 import하여 === 와 =≠ 메소드를 사용할 수도 있습니다.

import cats.syntax.eq._

123 === 123 //true
123 =!= 234 //true
123 === "123" //error : type mismatch

 

이제 Option[Int] 를 예를 들어 Eq 타입 클래스를 조금 더 살펴보겠습니다.

import cats.instances.int._
import cats.instances.option._

이렇게 두 개의 인스턴스를 import 해 준 다음, 다음 코드를 실행해 봅니다.

Some(1) === None //error : value === is not a member of Some[iIt]

 

위 코드는 Int 와 Option[Int]를 import하고 Some[Int]를 비교하고자 했기 때문에 에러가 발생합니다.

이를 해결하고자 한다면 비교하려는 값들을 Option[Int] 타입으로 설정해 주어야 합니다.

Option(1) === Option.empty[Int] //false

 

또는 다음과 같이 cats.syntax.option 을 사용해도 됩니다.

import cats.syntax.option._

1.some === none[Int] //false
1.some =!= none[Int] //true

 

그리고 위 Show 타입 클래스와 유사하게 (A, A) ⇒ Boolean 타입의 함수를 매개변수로 넘겨 사용자가 생성한 타입에 대해 Eq 인스턴스를 생성할 수 있습니다.

import cats.instances.string._

final case class User(name: String, mail: String)

implicit val userEq: Eq[User] = 
	Eq.instance[User] { (user1, user2) => 
		(user1.getName === user2.getName) && (user1.getMail === user2.getMail)
	}

val sean = new User("Sean", "sean@mail.com")
val jay = new User("Jay", "jay@mail.com")

sean === sean //true
sean === jay //false

 

반응형