6

Mam sytuacji, gdy trzeba metodę, która może wziąć w rodzaju:Scala rekurencyjne rodzaj i typ konstruktor realizacja

Array[Int] 
Array[Array[Int]] 
Array[Array[Array[Int]]] 
Array[Array[Array[Array[Int]]]] 
etc... 

nazwijmy ten typ RAI dla „rekurencyjnego tablicy int”

def make(rai: RAI): ArrayPrinter = { ArrayPrinter(rai) } 

Gdzie ArrayPrinter jest klasą, która jest inicjowana z RAI i iteruje przez całą ramkę (załóżmy, że drukuje wszystkie wartości w tym Array [Array [Int]])

val arrayOfArray: Array[Array[Int]] = Array(Array(1, 2), Array(3, 4)) 
val printer: ArrayPrinter[Array[Array[Int]]] = make(arrayOfArray) 
printer.print_! // prints "1, 2, 3, 4" 

Może również zwracać oryginalny Array [Array [Int]] bez utraty informacji o typie.

val arr: Array[Array[Int]] = printer.getNestedArray() 

Jak zrealizować to w Scala?

+1

Brzmi jak zadanie dla https://index.scala-lang.org/slamdata/matryoshka/matryoshka-core/0.18.3 – Reactormonk

+0

Same diagramy są warte sprawdzenia matryoshka! – pedrofurla

Odpowiedz

3

Skoncentrujmy się najpierw na typie. Według definicji, typu T powinny typecheck jako argument dla ArrayPrinter jest to akceptowane przez następującą funkcję typu:

def accept[T]: Boolean = 
    T match { // That's everyday business in agda 
    case Array[Int] => true 
    case Array[X] => accept[X] 
    case _   => false 
    } 

w Scala, można zakodować tę funkcję typu użyciu niejawny rozdzielczość:

trait RAI[T] 

object RAI { 
    implicit val e0: RAI[Array[Int]] = null 
    implicit def e1[T](implicit i: RAI[T]): RAI[Array[T]] = null 
} 

case class ArrayPrinter[T: RAI](getNestedArray: T) // Only compiles it T is a RAI 

aby wydrukować rzeczy najprostsze rozwiązanie jest traktowanie rai: T jako rai: Any:

def print_!: Unit = { 
    def print0(a: Any): Unit = a match { 
    case a: Int  => println(a) 
    case a: Array[_] => a.foreach(print0) 
    case _   => ??? 
    } 
} 

Możesz też mieć ochotę i napisać print_! używając klas typu, ale to prawdopodobnie byłoby mniej wydajne i wymagałoby więcej czasu na pisanie niż powyższe ... Pozostawione jako ćwiczenie dla czytelnika ;-)

0

To jest sposób zwykle robi się to poprzez zdefiniowanie klasy abstrakcyjnej, która zawiera wszystkie funkcje, które chciałbyś powiązać z tym typem rekursywnym, ale w rzeczywistości nie pobiera żadnych argumentów konstruktora. Przeciwnie, wszystkie jego metody przyjmują (przynajmniej jeden z) typ jako argument. Przykładem kanonicznym będzie Ordering. Zdefiniuj jedną lub więcej niejawnych implementacji tej klasy, a następnie za każdym razem, gdy jej potrzebujesz, zaakceptuj ją jako niejawny parametr. Odpowiedni przykład to List's sorted method.

W twoim przypadku może to wyglądać tak:

abstract class ArrayPrinter[A] { 
    def mkString(a: A): String 
} 
implicit object BaseArrayPrinter extends ArrayPrinter[Int] { 
    override def mkString(x: Int) = x.toString 
} 
class WrappedArrayPrinter[A](wrapped: ArrayPrinter[A]) extends ArrayPrinter[Array[A]] { 
    override def mkString(xs: Array[A]) = xs.map(wrapped.mkString).mkString(", ") 
} 
implicit def makeWrappedAP[A](implicit wrapped: ArrayPrinter[A]): ArrayPrinter[Array[A]] = new WrappedArrayPrinter(wrapped) 

def printHello[A](xs: A)(implicit printer: ArrayPrinter[A]): Unit = { 
    println("hello, array: " + printer.mkString(xs)) 
} 

To wydaje się być nieco czystsze niż posiadające że RAIOps klasy (lub ArrayPrinter) podjąć w obiekcie jako część jego konstruktora. Zwykle prowadzi to do "boksowania" i "rozpakowywania", skomplikowanych sygnatur typów, dziwnego dopasowywania wzorców, itp.

Ma również dodatkową zaletę, że łatwiej je rozciągnąć. Jeśli później ktoś inny ma powód, by chcieć implementacji ArrayPrinter dla Set[Int], może zdefiniować go lokalnie na swoim kodzie. Wiele razy definiowałem niestandardowy Ordering.