HList는 복잡한 튜플 작성 방법에 지나지 않습니까?


144

나는 차이점이 어디에 있는지, 더 일반적으로 HList를 사용할 수없는 정규 사용 사례를 식별하기 위해 (또는 일반 목록에 비해 어떤 이점도 제공하지 않는) 알아내는 데 정말로 관심이 있습니다.

( TupleN스칼라 에는 22 개 (믿습니다) 가 있지만 하나의 HList 만 필요하지만 이것이 내가 관심있는 개념적 차이는 아니라는 것을 알고 있습니다.)

아래 텍스트에 몇 가지 질문을 표시했습니다. 실제로 그들에게 대답 할 필요는 없으며, 그들은 나에게 분명하지 않은 것을 지적하고 특정 방향으로 토론을 인도하는 것이 더 의미가 있습니다.

자극

사람들이 HLists를 사용하도록 제안 곳 (예를 들어 의해 제공, 최근 SO에 대한 답변의 몇 가지를 본 적이 볼품 에 삭제 된 답변을 포함) 이 질문에 . 그것은 이 논의 를 일으켜서이 질문을 촉발시켰다.

소개

hlists는 요소 수와 정확한 유형을 정적으로 알고있는 경우에만 유용합니다. 숫자는 실제로 중요하지는 않지만 다양하지만 정적으로 정확하게 알려진 유형의 요소로 목록을 생성해야 할 필요는 없지만 정적으로 숫자를 모르는 것 같습니다. 질문 1 : 예를 들어 루프와 같은 예제를 작성할 수 있습니까? 내 직감은 정적으로 알려지지 않은 수의 임의의 요소 (주어진 클래스 계층 구조에 임의의)를 가진 정적으로 정확한 hlist를 갖는 것은 호환되지 않는다는 것입니다.

HLists vs. 튜플

이것이 사실이라면, 즉 당신은 정적으로 숫자와 유형을 알고 있습니다- 질문 2 : 왜 n- 튜플을 사용하지 않습니까? 물론, 당신은 typesafely 매핑 할 수 있습니다와 HList을 통해 배 (이 또한 할 수 있지만 하지 typesafely의 도움으로 튜플을 통해 할 productIterator)하지만, 정적으로 알려진 요소의 수와 유형 때문에 당신은 아마 튜플 요소에 액세스 할 수 있습니다 직접 작업을 수행하십시오.

반면에, fhlist에 매핑 하는 함수 가 너무 일반적인 경우 모든 요소를 ​​받아 들일 수 있습니다 – 질문 3 : 왜 그것을 통해 사용하지 productIterator.map않습니까? 메서드 오버로딩에서 흥미로운 차이점이 하나 있습니다. 오버로드가 여러 개인 경우 fhIT에서 제공하는 더 강력한 유형 정보 (productIterator와 달리)를 사용하면 컴파일러가 더 구체적으로 선택할 수 있습니다 f. 그러나 메소드와 함수가 동일하지 않기 때문에 실제로 Scala에서 작동하는지 확실하지 않습니다.

H 목록 및 사용자 입력

동일한 가정, 즉, 요소의 수와 유형을 정적으로 알아야한다는 가정- 질문 4 : 요소가 모든 종류의 사용자 상호 작용에 의존하는 상황에서 hlists를 사용할 수 있습니까? 예를 들어, 루프 내부에 요소로 hlist를 채우는 것을 상상해보십시오. 요소는 특정 조건이 유지 될 때까지 어딘가 (UI, 구성 파일, 행위자 상호 작용, 네트워크)에서 읽습니다. hlist의 유형은 무엇입니까? 인터페이스 사양 getElements : HList [...]와 유사하며 정적으로 알려지지 않은 길이의 목록과 함께 작동해야하며 시스템의 구성 요소 A가 구성 요소 B에서 임의의 요소 목록을 가져올 수 있습니다.

답변:


144

1-3 가지 문제를 해결합니다. 주요 응용 프로그램 중 하나는 HListsarity를 ​​추상화하는 것입니다. Arity는 일반적으로 특정 추상화 사용 사이트에서 정적으로 알려져 있지만 사이트마다 다릅니다. shapeless의 에서 이것을 취하십시오 .

def flatten[T <: Product, L <: HList](t : T)
  (implicit hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =
    flatten(hl(t))

val t1 = (1, ((2, 3), 4))
val f1 = flatten(t1)     // Inferred type is Int :: Int :: Int :: Int :: HNil
val l1 = f1.toList       // Inferred type is List[Int]

val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
// Inferred type of t2b is (Int, Boolean, Double, String, String, Int, Boolean)

HLists튜플 인수의 arity를 ​​추상화하기 위해 (또는 이와 동등한 것)을 사용하지 않으면 flatten이 두 가지 매우 다른 모양의 인수를 허용하고 형식이 안전한 방식으로 변환 할 수있는 단일 구현을 갖는 것이 불가능합니다.

고정 된 자족이 관여하는 모든 곳에서, 위와 같이, 방법 / 함수 매개 변수 목록과 사례 클래스를 포함하는, 튜플에 대해 추상화하는 능력은 관심이있을 것입니다. 타입 클래스 인스턴스를 거의 자동으로 얻기 위해 임의의 케이스 클래스의 특성을 추상화하는 방법에 대한 예제는 여기 를 참조 하십시오 .

// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)

// Publish their `HListIso`'s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)

// And now they're monoids ...

implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))

implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))

어떤 런타임 없습니다 반복은 여기에 있지만,이 중복 의 사용, HLists(또는 이에 상응하는 구조)를 제거 할 수는. 물론 반복적 인 상용구에 대한 내성이 높으면 관심있는 각 모양마다 여러 가지 구현을 작성하여 동일한 결과를 얻을 수 있습니다.

세 번째 질문에서는 "... hlist에 매핑하는 함수 f가 너무 일반적인 것이 모든 요소를 ​​받아 들일 수있는 경우 productIterator.map을 통해 사용하지 않는 이유는 무엇입니까?"입니다. HList를 통해 매핑하는 함수가 실제로 양식 Any => T이면 매핑을 통해 productIterator완벽하게 사용할 수 있습니다. 그러나 형식의 기능은 Any => T일반적으로 그다지 흥미롭지 않습니다 (적어도 내부적으로 캐스트하지 않는 한 아닙니다). shapeless는 다형성 함수 값의 형태를 제공하여 컴파일러가 의심스러운 방식으로 유형별 사례를 선택할 수 있도록합니다. 예를 들어

// size is a function from values of arbitrary type to a 'size' which is
// defined via type specific cases
object size extends Poly1 {
  implicit def default[T] = at[T](t => 1)
  implicit def caseString = at[String](_.length)
  implicit def caseList[T] = at[List[T]](_.length)
}

scala> val l = 23 :: "foo" :: List('a', 'b') :: true :: HNil
l: Int :: String :: List[Char] :: Boolean :: HNil =
  23 :: foo :: List(a, b) :: true :: HNil

scala> (l map size).toList
res1: List[Int] = List(1, 3, 2, 1)

사용자 입력에 관한 질문 4와 관련하여 고려해야 할 두 가지 경우가 있습니다. 첫 번째는 알려진 정적 조건을 얻도록 보장하는 컨텍스트를 동적으로 설정할 수있는 상황입니다. 이러한 종류의 시나리오에서는 형태가없는 기술을 완벽하게 적용 할 수 있지만, 정적 조건 런타임에 얻어 지지 않으면 대체 경로를 따라야 한다는 점이 분명 합니다. 당연히, 이는 동적 조건에 민감한 방법이 선택적인 결과를 가져와야한다는 것을 의미합니다. 다음은 HLists 를 사용하는 예입니다 .

trait Fruit
case class Apple() extends Fruit
case class Pear() extends Fruit

type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil

val a : Apple = Apple()
val p : Pear = Pear()

val l = List(a, p, a, p) // Inferred type is List[Fruit]

의 유형은 l목록의 길이 또는 요소의 정확한 유형을 캡처하지 않습니다. 그러나 특정 형식 (예 : 알려진 고정 스키마를 준수해야 함)을 예상 할 경우 해당 사실을 설정하고 그에 따라 조치를 취할 수 있습니다.

scala> import Traversables._
import Traversables._

scala> val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]
res0: Option[Apple :: Pear :: Apple :: Pear :: HNil] =
  Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)

scala> apap.map(_.tail.head)
res1: Option[Pear] = Some(Pear())

주어진 목록의 실제 길이가 다른 목록과 같은 길이라는 것을 제외하고는 다른 상황이 있습니다. 다시 말하지만, 이것은 완전히 정적으로 그리고 위에서와 같이 혼합 된 정적 / 동적 컨텍스트에서 형태없는 지원입니다. 여기를 참조 하십시오확장 된 예는 를 .

관찰 한 바와 같이, 이러한 모든 메커니즘은 최소한 조건부로 정적 유형 정보를 사용할 수 있어야하며 외부에서 제공되는 유형이 지정되지 않은 데이터에 의해 완전히 구동되는 완전히 동적 환경에서 이러한 기술을 사용할 수없는 것으로 보입니다. 그러나 2.10에서 스칼라 리플렉션의 구성 요소로 런타임 컴파일이 지원됨에 따라 이는 더 이상 극복 할 수없는 장애물이 아닙니다 ... 런타임 컴파일을 사용하여 가벼운 스테이징 형태를 제공하고 런타임에 정적 타이핑을 수행 할 수 있습니다 동적 데이터에 대한 응답 : 아래에서 발췌 한 내용 ... 전체 예제를 보려면 링크를 따르십시오.

val t1 : (Any, Any) = (23, "foo") // Specific element types erased
val t2 : (Any, Any) = (true, 2.0) // Specific element types erased

// Type class instances selected on static type at runtime!
val c1 = stagedConsumeTuple(t1) // Uses intString instance
assert(c1 == "23foo")

val c2 = stagedConsumeTuple(t2) // Uses booleanDouble instance
assert(c2 == "+2.0")

나는 의존적으로 유형이 지정된 프로그래밍 언어에 대한 그의 현명한 의견을 감안할 때 @PLT_Borat 가 그것에 대해 말할 것이 있다고 확신 합니다. ;-)


2
나는 당신의 대답의 마지막 부분에 약간 당황합니다. 그러나 매우 흥미 롭습니다! 당신의 큰 답변과 많은 참고 문헌에 감사드립니다. 내가 할 일이 많이있는 것처럼 보입니다 :-)
Malte Schwerhoff

1
arity에 대한 추상화는 매우 유용합니다. 슬프게도 ScalaMock은 다양한 FunctionN특성이 arity를 ​​통해 추상화하는 방법을 모르기 때문에 상당한 중복을 겪습니다 . github.com/paulbutcher/ScalaMock/blob/develop/core/src/main/… github.com/paulbutcher/ScalaMock/blob / 개발 / 코어 / SRC / 메인 / ... 슬프게도 나는 내가 "진짜"를 처리 할 필요가 주어진,이를 방지하기 위해 볼품를 사용할 수있는 방법을 알고 아니에요 FunctionN
폴 부처

1
나는 (꽤 인공적인) 예 -ideone.com/sxIw1을 만들었습니다 . 이것은 "동적 데이터에 대한 응답으로 런타임에 수행되는 정적 타이핑"과 결합하여 hlists에서 이점을 얻을 수 있습니까? (나는 아직도 후자가 정확히 무엇인지 확실하지 않다)
Malte Schwerhoff

17

분명히, HList는 본질적 Tuple2으로 약간 다른 설탕 이 쌓인 것 이상 입니다.

def hcons[A,B](head : A, tail : B) = (a,b)
def hnil = Unit

hcons("foo", hcons(3, hnil)) : (String, (Int, Unit))

따라서 귀하의 질문은 본질적으로 중첩 된 튜플과 플랫 튜플을 사용하는 것의 차이점에 관한 것이지만 두 가지는 동형이므로 결국 라이브러리 기능을 사용할 수 있고 표기법을 사용할 수있는 편리함을 제외하고는 아무런 차이가 없습니다.


튜플은 hlists에 매핑 될 수 있고 어쨌든 되돌릴 수 있으므로 명확한 동형이 있습니다.
Erik Kaplun

10

튜플로 할 수없는 일이 많이 있습니다.

  • 일반적인 prepend / add 함수를 작성
  • 역함수를 쓰다
  • concat 함수를 작성하십시오
  • ...

물론 튜플로 모든 것을 할 수 있지만 일반적인 경우는 아닙니다. 따라서 HLists를 사용하면 코드가 더 건조 해집니다.


8

나는 이것을 매우 간단한 언어로 설명 할 수 있습니다.

튜플 대 목록 이름은 중요하지 않습니다. HList는 HTuples로 명명 될 수 있습니다. 차이점은 Scala + Haskell에서 튜플 (스칼라 구문 사용)로이 작업을 수행 할 수 있다는 것입니다.

def append2[A,B,C](in: (A,B), v: C) : (A,B,C) = (in._1, in._2, v)

모든 유형의 정확히 두 요소의 입력 튜플을 가져 와서 세 번째 요소를 추가하고 정확히 세 개의 요소가 포함 된 완전한 유형의 튜플을 반환합니다. 그러나 이것은 유형에 대해 완전히 일반적이지만 입력 / 출력 길이를 명시 적으로 지정해야합니다.

Haskell 스타일 HList로 할 수있는 것은 길이에 따라이 일반을 만드는 것입니다. 따라서 모든 길이의 튜플 /리스트에 추가하고 완전히 정적으로 유형이 지정된 튜플 /리스트를 다시 가져올 수 있습니다. 이 이점은 또한 정확히 n 개의 정수 목록에 정수를 추가하고 n을 명시 적으로 지정하지 않고 정확히 (n + 1) 개의 정수를 갖도록 정적으로 형식화 된 목록을 가져올 수있는 동종 형식의 컬렉션에도 적용됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.