Статьи

Объясненный класс типов Scala: реализовать функцию String.read

В этой короткой статье я хотел бы объяснить, как вы можете использовать шаблон классов типов в Scala для реализации adhoc полиморфизма. Мы не будем вдаваться в теоретические подробности этой статьи, главное, что я хочу показать вам, — насколько легко на самом деле создать гибкую расширяемую модель без привязки вашей доменной модели к конкретной реализации или особенностям. Есть много ресурсов, касающихся классов типов scala, доступных в Интернете, но я не смог найти хорошую ссылку, поэтому я создал этот. В этом примере мы рассмотрим очень прагматичную (и наивную) реализацию класса чтения типов из haskell. Этот класс типов позволяет преобразовывать строку в определенный тип универсальным способом.

С этим классом типов мы хотим добиться следующего (полный пример можно найти здесь: https://gist.github.com/josdirksen/9051baf09003dac37386 ).

01
02
03
04
05
06
07
08
09
10
11
12
println(Readable[Double].read("10"))
println(Readable[Int].read("10"))
println(Readable[String].read("Well duh!"))
println(Readable[List[Char]].read("Well duh!"))
println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))
 
// we can also use the read function directly
println("20".read[Double]);
println("Using:A:Separator:to:split:a:String".read[List[Char]]);
println("Using:A:Separator:to:split:a:String".read[List[String]]);
println(Readable[Task].read("10|Title Text|Title Content"))
println("20|Another title Text|Another title Content".read[Task])

В классе типов Readable мы предоставляем универсальный способ преобразования строки в определенный тип. В приведенном выше примере мы использовали класс типов для преобразования строки в некоторые базовые типы, а также в другие списки и конкретный класс case. Функциональность для базовых типов на самом деле не так полезна, так как объект Scala String уже предоставляет функции toDouble, toString и т. Д. Таким образом, однако, вам не нужно знать конкретную функцию для вызова, но вы можете просто указать желаемый тип цели 🙂 Это, однако, становится намного интереснее с классом Case, который вы видите здесь. Как вы увидите в остальной части кода, с помощью имплицитов мы можем просто добавить поддержку этого класса case, не изменяя реализацию класса String или класса Task.

Чтобы реализовать класс типов, мы сначала должны определить черту, которая определяет функции, которые мы должны реализовать. В этом примере у нас есть только одна функция:

1
2
3
4
5
6
7
8
/**
 * The readable trait defines how objects can be converted from a string
 * representation to the objects instance. For most of the standard types
 * we can simply use the toType function of String.
 */
trait Readable[T] {
  def read(x: String): T
}

Функция read должна просто преобразовать String в указанный тип T. Теперь, когда мы определили нашу черту, давайте посмотрим на объект-компаньон, который содержит некоторые вспомогательные классы и простые реализации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * Companion object containing helper functions and standard implementations
 */
object Readable {
 
  /**
   * Helper function which allows creation of Readable instances
   */
  def toReadable[T](p: String => T): Readable[T] = new Readable[T] {
    def read(x: String): T = p(x)
  }
 
  /**
   * Allow for construction of standalone readables, if the ops aren't used
   */
  def apply[A](implicit instance: Readable[A]): Readable[A] = instance
 
  // Using the toReadable creates cleaner code, we could also explicitly
  // define the implicit instances:
  //
  //   implicit object ReadableDouble extends Readable[Double] {
  //      def read(s: String): Double = s.toDouble
  //    }
  //    implicit object ReadableInt extends Readable[Int] {
  //      def read(s: String): Int = s.toInt
  //    }
  implicit val ReadableDouble = toReadable[Double](_.toDouble)
  implicit val ReadableInt = toReadable[Int](_.toInt)
  implicit val ReadableLong = toReadable[Long](_.toLong)
  implicit val ReadableString = toReadable[String](new String(_))
  implicit val ReadableBoolean = toReadable[Boolean](_.toBoolean)
  implicit val ReadableCharList = toReadable[List[Char]](_.toCharArray.toList)
  implicit val ReadableStringList = toReadable[List[String]](_.split(':').toList)
 
}

Код не должен быть таким сложным для понимания. Что мы делаем здесь, так это создаем ряд читаемых реализаций. Важно отметить, что мы используем неявный параметр, чтобы позже мы могли включить его в область видимости. На данный момент мы уже можем начать использовать класс типов. Для этого мы импортируем имплициты и используем класс типа Readable следующим образом:

01
02
03
04
05
06
07
08
09
10
import Readable._
import Readable.ops._
 
// now we can just get an instance of a readable and call the read function
// to parse a string to a specific type.
println(Readable[Double].read("10"))
println(Readable[Int].read("10"))
println(Readable[String].read("Well duh!"))
println(Readable[List[Char]].read("Well duh!"))
println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))

Легко ли? Scala будет искать неявное значение, соответствующее указанному типу, и преобразовывать строку в этот тип. Это хорошо, но на самом деле выглядит не так хорошо. Нам нужно создать экземпляр Readable (даже если это не слишком много кода) и вызвать read для преобразования String. Мы можем сделать это проще, расширив класс String функцией чтения, которая выполняет преобразование за нас. Для этого добавьте следующее в объект Readable.

01
02
03
04
05
06
07
08
09
10
11
12
13
/**
 * Extend the string object with a read function.
 */
object ops {
  implicit class pp[T](s: String) {
 
    /**
     * The type parameter should have an implcit Readable in scope. Use
     * implicitly to access it and call the read function
     */
    def read[T: Readable]= implicitly[Readable[T]].read(s)
  }
}

Эти операции определяют неявное преобразование, которое добавляет функцию чтения объекта String. Осталось только импортировать неявные функции ops, и мы можем использовать чтение напрямую:

1
2
3
4
5
import Readable.ops._
// we can also use the read function directly
println("20".read[Double]);
println("Using:A:Separator:to:split:a:String".read[List[Char]]);
println("Using:A:Separator:to:split:a:String".read[List[String]]);

Этот же подход также можно использовать для создания функции чтения для case-классов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// creating custom read function can be done without tying a case class
// to the reads implementation. In the following sample assume we
// serialize it to a string with | as separators:
//     10|Title Text|Title Content
case class Task(id: Long, title: String, content: String)
 
// simple convert the incoming string to a Task
implicit val readableTask = toReadable(
  _.split('|') match {
    case Array(id: String, title: String, content: String) => new Task(id.read[Long], title, content)
  }
)
 
println(Readable[Task].read("10|Title Text|Title Content"))
println("20|Another title Text|Another title Content".read[Task])

И это в значительной степени так. Путем простой реализации свойства Readable [Task] наш пользовательский класс Case Task можно обрабатывать так же, как и другие объекты.