HList는 복잡한 튜플 작성 방법에 지나지 않습니까?
나는 차이점이 어디에 있는지, 더 일반적으로 HList를 사용할 수없는 정규 사용 사례를 식별하는 데 관심이 있고 (또는 일반 목록에 비해 이점을 얻지 않음) 관심이 있습니다.
( TupleN
스칼라 에는 22 개 (믿습니다) 가 있지만 하나의 HList 만 필요하지만 이것이 내가 관심있는 개념적 차이는 아니라는 것을 알고 있습니다.)
아래 텍스트에 몇 가지 질문을 표시했습니다. 실제로 그들에게 대답 할 필요는 없으며, 나에게 불분명 한 것을 지적하고 특정 방향으로 토론을 인도하는 것이 더 의미가 있습니다.
자극
사람들이 HLists를 사용하도록 제안 곳 (예를 들어 의해 제공, 최근 SO에 대한 답변의 몇 가지를 본 적이 볼품 에 삭제 된 답변을 포함) 이 질문에 . 그것은 이 논의 를 일으켜서이 질문을 촉발시켰다.
소개
hlists는 요소 수와 정확한 유형을 정적으로 알고있는 경우에만 유용합니다. 숫자는 실제로 중요하지는 않지만 다양하지만 정적으로 정확하게 알려진 유형의 요소로 목록을 생성해야 할 필요는 없지만 정적으로 숫자를 모르는 것 같습니다. 질문 1 : 예를 들어 루프와 같은 예제를 작성할 수 있습니까? 내 직감은 정적으로 알려지지 않은 수의 임의의 요소 (주어진 클래스 계층 구조에 대한 임의의)를 가진 정적으로 정확한 hlist를 갖는 것은 호환되지 않는다는 것입니다.
HLists vs. 튜플
이것이 사실이라면, 즉 당신은 정적으로 숫자와 유형을 알고 있습니다- 질문 2 : 왜 n- 튜플을 사용하지 않습니까? 물론, 당신은 typesafely 매핑 할 수 있습니다와 HList을 통해 배 (이 또한 할 수 있지만 하지 typesafely의 도움으로 튜플을 통해 할 productIterator
)하지만, 정적으로 알려진 요소의 수와 유형 때문에 당신은 아마 튜플 요소에 액세스 할 수 있습니다 직접 작업을 수행하십시오.
한편, 기능 f
당신이 hlist을 통해지도는 모든 요소 받아들이는 너무 일반적입니다 - 질문 3 : 왜 안 통해 그것을 사용 productIterator.map
? 메서드 오버로딩에서 흥미로운 차이점이 하나 있습니다. 오버로드가 여러 개인 경우 f
hIT에서 제공하는 더 강력한 유형 정보 (productIterator와 달리)를 사용하면 컴파일러가 더 구체적으로 선택할 수 있습니다 f
. 그러나 메소드와 함수가 동일하지 않기 때문에 실제로 Scala에서 작동하는지 확실하지 않습니다.
H 목록 및 사용자 입력
동일한 가정을 바탕으로, 즉 요소의 수와 유형을 정적으로 알아야한다는 질문 4 : 질문 4 : 요소가 모든 종류의 사용자 상호 작용에 의존하는 상황에서 hlists를 사용할 수 있습니까? 예를 들어, 루프 내부에 요소로 hlist를 채우는 것을 상상해보십시오. 요소는 특정 조건이 유지 될 때까지 어딘가 (UI, 구성 파일, 행위자 상호 작용, 네트워크)에서 읽습니다. hlist의 유형은 무엇입니까? 인터페이스 사양 getElements : HList [...]와 유사하며 정적으로 알려지지 않은 길이의 목록과 함께 작동해야하며 시스템의 구성 요소 A가 구성 요소 B에서 임의의 요소 목록을 가져올 수 있습니다.
1-3 가지 문제를 해결합니다. 주요 응용 프로그램 중 하나는 HLists
arity를 추상화하는 것입니다. 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
(또는 동등한 구조)를 사용하면 제거 할 수 있습니다. 물론 반복적 인 상용구에 대한 내성이 높으면 관심있는 각 모양마다 여러 가지 구현을 작성하여 동일한 결과를 얻을 수 있습니다.
In question three you ask "... if the function f you map over an hlist is so generic that it accepts all elements ... why not use it via productIterator.map?". If the function you map over an HList really is of the form Any => T
then mapping over productIterator
will serve you perfectly well. But functions of the form Any => T
aren't typically that interesting (at least, they aren't unless they type cast internally). shapeless provides a form of polymorphic function value which allows the compiler to select type-specific cases in exactly the way you're doubtful about. For instance,
// 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)
With respect to your question four, about user input, there are two cases to consider. The first is situations where we can dynamically establish a context which guarantees that a known static condition obtains. In these kinds of scenarios it's perfectly possible to apply shapeless techniques, but clearly with the proviso that if the static condition doesn't obtain at runtime then we have to follow an alternative path. Unsurprisingly, this means that methods which are sensitive to dynamic conditions have to yield optional results. Here's an example using HList
s,
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]
The type of l
doesn't capture the length of the list, or the precise types of its elements. However, if we expect it to have a specific form (ie. if it ought to conform to some known, fixed schema), then we can attempt to establish that fact and act accordingly,
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())
There are other situations where we might not care about the actual length of a given list, other than that it is the same length as some other list. Again, this is something that shapeless supports, both fully statically, and also in a mixed static/dynamic context as above. See here for an extended example.
It is true, as you observe, that all of these mechanism require static type information to be available, at least conditionally, and that would seem to exclude these techniques from being usable in a completely dynamic environment, fully driven by externally provided untyped data. But with the advent of the support for runtime compilation as a component of Scala reflection in 2.10, even this is no longer an insuperable obstacle ... we can use runtime compilation to provide a form of lightweight staging and have our static typing performed at runtime in response to dynamic data: excerpt from the preceding below ... follow the link for the full example,
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")
I'm sure @PLT_Borat will have something to say about that, given his sage comments about dependently typed programming languages ;-)
Just to be clear, an HList is essentially nothing more than a stack of Tuple2
with slightly different sugar on top.
def hcons[A,B](head : A, tail : B) = (a,b)
def hnil = Unit
hcons("foo", hcons(3, hnil)) : (String, (Int, Unit))
So your question is essentially about the differences between using nested tuples vs flat tuples, but the two are isomorphic so in the end there really is no difference except convenience in which library functions can be used and which notation can be used.
There are a lot of things you can't do (well) with tuples:
- write a generic prepend/append function
- write a reverse function
- write a concat function
- ...
You can do all of that with tuples of course, but not in the general case. So using HLists makes your code more DRY.
I can explain this in super simple language:
The tuple vs list naming isn't significant. HLists could be named as HTuples. The difference is that in Scala+Haskell, you can do this with a tuple (using Scala syntax):
def append2[A,B,C](in: (A,B), v: C) : (A,B,C) = (in._1, in._2, v)
to take an input tuple of exactly two elements of any type, append a third element, and return a fully typed tuple with exactly three elements. But while this is completely generic over types, it has to explicitly specify the input/output lengths.
What a Haskell style HList lets you do is make this generic over length, so you can append to any length of tuple/list and get back a fully statically typed tuple/list. This benefit also applies to homogeneously typed collections where you can append an int to a list of exactly n ints and get back a list that is statically typed to have exactly (n+1) ints without explicitly specifying n.
'development' 카테고리의 다른 글
iPad Web App : Safari에서 JavaScript를 사용하여 가상 키보드 감지? (0) | 2020.06.20 |
---|---|
Firefox 애드온은 어떻게 작성합니까? (0) | 2020.06.20 |
“#define _GNU_SOURCE”는 무엇을 의미합니까? (0) | 2020.06.20 |
배쉬 연속 라인 (0) | 2020.06.20 |
네비게이션 컨트롤러 스택, 서브 뷰 또는 모달 컨트롤러를 사용하지 않고 뷰 컨트롤러의 애니메이션 변경? (0) | 2020.06.20 |