haskell 유니코드 - 형식을 데이터 생성자와 연결시키는 ADT 인코딩의 문제점은 무엇입니까?(예:Scala.)




한글 제거 (2)

필자가 아는 한, 스칼라의 케이스 클래스의 관용적 인 인코딩은 타입 유추와 타입 특이성을 악용 할 수있는 두 가지 이유가있다. 전자는 구문상의 편의성의 문제이지만 후자는 추론의 범위가 확대 된 문제입니다.

하위 유형 지정 문제는 비교적 쉽게 설명 할 수 있습니다.

val x = Some(42)

x 의 타입은 Some[Int] 로 밝혀 Some[Int] , 아마 당신이 원하는 것이 아닙니다. 다른 문제가있는 다른 영역에서 유사한 문제를 생성 할 수 있습니다.

sealed trait ADT
case class Case1(x: Int) extends ADT
case class Case2(x: String) extends ADT

val xs = List(Case1(42), Case1(12))

xs 의 유형은 List[Case1] 입니다. 이것은 기본적으로 당신이 원하는 것이 아닌 것으로 보장 됩니다. 이 문제를 해결하기 위해 List 와 같은 컨테이너는 type 매개 변수에 공변수가 있어야합니다. 불행하게도, 공분산은 모든 문제를 야기하며 사실상 특정 구조물의 건전성을 떨어 뜨립니다 (예 : Scalaz의 Monad 유형 및 모나드 변압기의 공분산 컨테이너 허용).

따라서 이러한 방식으로 ADT를 인코딩하면 코드에 다소 바이러스 성 영향이 있습니다. ADT 자체에서 하위 유형 지정을 처리 할 필요가있을뿐만 아니라 작성한 모든 컨테이너는 부적절한 순간에 ADT의 하위 유형에 착수한다는 사실을 고려해야합니다.

공용 사례 클래스를 사용하여 ADT를 인코딩하지 않는 두 번째 이유는 "non-types"으로 형식 공간이 어지럽히는 것을 방지하기 위해서입니다. 특정 관점에서 ADT 사례는 실제로 유형이 아닙니다. 데이터 유형입니다. 이런 방식으로 ADT를 추론하면 (잘못된 것은 아닙니다!) ADT 사례 각각에 대해 일류 유형을 사용하면 코드에 대한 추론을 위해 수행해야하는 일련의 작업이 늘어납니다.

예를 들어 위의 ADT 대수를 생각해보십시오. 이 ADT를 사용하는 코드에 대해 추론하려면 "이 유형이 Case1 이면 무엇입니까?"에 대해 끊임없이 생각해야합니다. Case1 이 데이터이기 때문에 누가 물어볼 필요 가있는 질문이 아닙니다. 특정 제품에 대한 태그입니다. 그게 다야.

개인적으로, 나는 위의 것들에 대해별로 신경 쓰지 않습니다. 내 생각에 공분산에 대한 건전성 문제는 사실이지만 일반적으로 컨테이너를 불변으로 만들고 내 사용자에게 "빨아서 유형에 주석을 달"라고 지시합니다. 불편하고 벙어리지만 대체로 더 많은 상용구 접기와 "소문자"데이터 생성자가 선호됩니다.

와일드 카드로 이러한 유형의 유형에 대한 세 번째 잠재적 단점은 개별 ADT 유형에 대 / 소문자 관련 기능을 넣는 "객체 지향"스타일을 장려합니다 (오히려 허용). 나는이 방법으로 당신의 은유 (케이스 클래스 대 서브 타입 다형성)를 혼합하는 것은 나쁜 것에 대한 처방이라고 생각하는 것은 거의 없다고 생각합니다. 그러나이 결과가 유형화 된 사례의 잘못인지 여부는 공개적인 질문입니다.

스칼라에서 대수 데이터 유형은 sealed 1 레벨 유형 계층 구조로 인코딩됩니다. 예:

-- Haskell
data Positioning a = Append
                   | AppendIf (a -> Bool)
                   | Explicit ([a] -> [a]) 
// Scala
sealed trait Positioning[A]
case object Append extends Positioning[Nothing]
case class AppendIf[A](condition: A => Boolean) extends Positioning[A]
case class Explicit[A](f: Seq[A] => Seq[A]) extends Positioning[A]

대 / case classcase object 사용하면 스칼라는 equals , hashCode , unapply (패턴 일치에 사용됨) 등과 같은 많은 것들을 생성하여 전통적인 ADT의 주요 특성 및 기능을 다양하게 제공합니다.

하나의 주요한 차이점이 있습니다 - 스칼라에서 "데이터 생성자"는 자체 유형을 가지고 있습니다 . 예를 들어 다음 두 가지를 비교하십시오 (각각의 REPL에서 복사 됨).

// Scala

scala> :t Append
Append.type

scala> :t AppendIf[Int](Function const true)
AppendIf[Int]

-- Haskell

haskell> :t Append
Append :: Positioning a

haskell> :t AppendIf (const True)
AppendIf (const True) :: Positioning a

필자는 항상 Scala 변형을 유리한쪽으로 간주했습니다.

결국 유형 정보가 손실되지 않습니다 . AppendIf[Int] 는 예를 들어 Positioning[Int] 의 하위 유형입니다.

scala> val subtypeProof = implicitly[AppendIf[Int] <:< Positioning[Int]]
subtypeProof: <:<[AppendIf[Int],Positioning[Int]] = <function1>

사실, 값에 대한 불변의 추가 컴파일 타임을 얻게 됩니다. (우리는 이것이 한정 타이핑의 제한된 버전이라고 부를 수 있을까요?)

이것은 좋은 사용법이 될 수 있습니다 - 일단 데이터 생성자가 값을 생성하는 데 사용되었다는 것을 알게되면 해당 유형을 나머지 흐름을 통해 전달하여 더 많은 유형 안전을 추가 할 수 있습니다. 예를 들어,이 스칼라 인코딩을 사용하는 JSON을 재생하면 임의의 JsValue 아닌 JsObject 에서 fields 를 추출 할 수 있습니다.

scala> import play.api.libs.json._
import play.api.libs.json._

scala> val obj = Json.obj("key" -> 3)
obj: play.api.libs.json.JsObject = {"key":3}

scala> obj.fields
res0: Seq[(String, play.api.libs.json.JsValue)] = ArrayBuffer((key,3))

scala> val arr = Json.arr(3, 4)
arr: play.api.libs.json.JsArray = [3,4]

scala> arr.fields
<console>:15: error: value fields is not a member of play.api.libs.json.JsArray
              arr.fields
                  ^

scala> val jsons = Set(obj, arr)
jsons: scala.collection.immutable.Set[Product with Serializable with play.api.libs.json.JsValue] = Set({"key":3}, [3,4])

하스켈에서, fields 는 아마도 JsValue -> Set (String, JsValue) 타입을 가질 것이다. 즉, JsArray 등에서 런타임시 실패하게됩니다.이 문제는 잘 알려진 부분 레코드 접근 자의 형태로 나타납니다.

스칼라가 데이터 생성자를 잘못 처리했다는 사실은 트위터, 메일 링리스트, IRC, SO 등에서 여러 번 표현되었다 . 불행히도 몇 가지를 제외하고는 그 어떤 것과도 링크가 없다. Travis Brown, Scala 용 순수 JSON 라이브러리 인 Argonaut .

Argonaut는 consciously 으로 Haskell 접근법을 사용한다 (사례 클래스를 private 하고 데이터 생성자를 수동으로 제공함으로써). 하스켈 인코딩으로 언급 한 문제는 Argonaut와 함께 존재한다는 것을 알 수 있습니다. (단, Option 을 사용하여 편미성을 나타냅니다.)

scala> import argonaut._, Argonaut._
import argonaut._
import Argonaut._

scala> val obj = Json.obj("k" := 3)
obj: argonaut.Json = {"k":3}

scala> obj.obj.map(_.toList)
res6: Option[List[(argonaut.Json.JsonField, argonaut.Json)]] = Some(List((k,3)))

scala> val arr = Json.array(jNumber(3), jNumber(4))
arr: argonaut.Json = [3,4]

scala> arr.obj.map(_.toList)
res7: Option[List[(argonaut.Json.JsonField, argonaut.Json)]] = None

나는 꽤 오랫동안 이것을 숙고 해왔지만 스칼라의 인코딩을 왜 잘못 만드는지 이해하지 못합니다. 물론 그것은 타입 추론을 방해 할 수 있습니다.하지만 잘못된 판단을 내릴만한 충분한 이유는 아닙니다. 내가 뭘 놓치고 있니?


이것이 적합한 해결 방법인지 궁금합니다.

scala> List(1,2,3) match {
     |    case List(_: String, _*) => println("A list of strings?!")
     |    case _ => println("Ok")
     | }

"빈 목록"의 경우와 일치하지 않지만 경고가 아닌 컴파일 오류가 발생합니다!

error: type mismatch;
found:     String
requirerd: Int

이것은 다른 한편으로는 작동하는 것처럼 보입니다 ....

scala> List(1,2,3) match {
     |    case List(_: Int, _*) => println("A list of ints")
     |    case _ => println("Ok")
     | }

좀 더 나아지지 않습니까? 아니면 요점을 놓치고 있습니까?





scala haskell playframework functional-programming argonaut