developer tip

암시 적 변환 대 유형 클래스

optionbox 2020. 9. 4. 07:10
반응형

암시 적 변환 대 유형 클래스


Scala에서는 기존 또는 새로운 유형을 개조하기 위해 적어도 두 가지 방법을 사용할 수 있습니다. 를 사용하여 무언가를 정량화 할 수 있음을 표현하고 싶다고 가정합니다 Int. 다음과 같은 특성을 정의 할 수 있습니다.

암시 적 변환

trait Quantifiable{ def quantify: Int }

그런 다음 암시 적 변환을 사용하여 예를 들어 문자열 및 목록을 수량화 할 수 있습니다.

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

이들을 가져온 후 quantify문자열과 목록 에서 메서드 호출 할 수 있습니다 . 수량화 가능한 목록은 길이를 저장하므로 후속 호출에서 목록의 값 비싼 순회를 방지합니다 quantify.

유형 클래스

대안은 Quantified[A]어떤 유형 A이 정량화 될 수 있음을 나타내는 "증인"을 정의하는 것입니다.

trait Quantified[A] { def quantify(a: A): Int }

우리는이 유형에 대한 클래스의 인스턴스 제공 String하고 List어딘가에.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

그리고 인수를 정량화해야하는 메서드를 작성하면 다음과 같이 작성합니다.

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

또는 컨텍스트 바운드 구문을 사용합니다.

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

그러나 언제 어떤 방법을 사용합니까?

이제 질문이옵니다. 이 두 가지 개념을 어떻게 결정할 수 있습니까?

내가 지금까지 알아 차린 것.

유형 클래스

  • 유형 클래스는 멋진 컨텍스트 바인딩 구문을 허용합니다.
  • 유형 클래스를 사용하면 사용할 때마다 새 래퍼 객체를 만들지 않습니다.
  • 유형 클래스에 여러 유형 매개 변수가있는 경우 컨텍스트 바운드 구문이 더 이상 작동하지 않습니다. 정수뿐만 아니라 일반적인 유형의 값으로도 수량화하고 싶다고 상상해보십시오 T. 유형 클래스를 만들고 싶습니다.Quantified[A,T]

암시 적 변환

  • 새 객체를 생성했기 때문에 값을 캐시하거나 더 나은 표현을 계산할 수 있습니다. 그러나 여러 번 발생할 수 있고 명시 적 변환이 한 번만 호출 될 수 있으므로 이것을 피해야합니까?

답변에서 기대하는 것

두 개념의 차이가 중요한 하나 (또는 ​​그 이상)의 사용 사례를 제시하고 왜 내가 다른 개념보다 선호하는지 설명하십시오. 또한 두 개념의 본질과 서로 간의 관계를 설명하는 것은 예가 없어도 좋을 것입니다.


Scala In Depth의 자료를 복제하고 싶지는 않지만 유형 클래스 / 유형 특성이 무한히 더 유연하다는 점에 주목할 가치가 있다고 생각합니다.

def foo[T: TypeClass](t: T) = ...

로컬 환경에서 기본 유형 클래스를 검색하는 기능이 있습니다. 그러나 다음 두 가지 방법 중 하나로 언제든지 기본 동작을 재정의 할 수 있습니다.

  1. 암시 적 조회를 단락시키기 위해 Scope에서 암시 적 유형 클래스 인스턴스 생성 / 가져 오기
  2. 유형 클래스 직접 전달

예를 들면 다음과 같습니다.

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

This makes type classes infinitely more flexible. Another thing is that type classes / traits support implicit lookup better.

In your first example, if you use an implicit view, the compiler will do an implicit lookup for:

Function1[Int, ?]

Which will look at Function1's companion object and the Int companion object.

Notice that Quantifiable is nowhere in the implicit lookup. This means you have to place the implicit view in a package object or import it into scope. It's more work to remember what's going on.

On the other hand, a type class is explicit. You see what it's looking for in the method signature. You also have an implicit lookup of

Quantifiable[Int]

which will look in Quantifiable's companion object and Int's companion object. Meaning that you can provide defaults and new types (like a MyString class) can provide a default in their companion object and it will be implicitly searched.

In general, I use type classes. They are infinitely more flexible for the initial example. The only place I use implicit conversions is when using an API layer between a Scala wrapper and a Java library, and even this can be 'dangerous' if you're not careful.


One criterion that can come into play is how you want the new feature to "feel" like; using implicit conversions, you can make it look like it is just another method:

"my string".newFeature

...while using type classes it will always look like it you are calling an external function:

newFeature("my string")

One thing that you can achieve with type classes and not with implicit conversions is adding properties to a type, rather than to an instance of a type. You can then access these properties even when you do not have an instance of the type available. A canonical example would be:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

This example also shows how the concepts are tightly related: type classes would not be nearly as useful if there were no mechanism to produce infinitely many of their instances; without the implicit method (not a conversion, admittedly), I could only have finitely many types have the Default property.


You can think of the difference between the two techniques by analogy to function application, just with a named wrapper. For example:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

An instance of the former encapsulates a function of type A => Int, whereas an instance of the latter has already been applied to an A. You could continue the pattern...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

thus you could think of Foo1[B] sort of like the partial application of Foo2[A, B] to some A instance. A great example of this was written up by Miles Sabin as "Functional Dependencies in Scala".

So really my point is that, in principle:

  • "pimping" a class (through implicit conversion) is the "zero'th order" case...
  • declaring a typeclass is the "first order" case...
  • multi-parameter typeclasses with fundeps (or something like fundeps) is the general case.

참고URL : https://stackoverflow.com/questions/8524878/implicit-conversion-vs-type-class

반응형