Статьи

Scala Implicits: введите классы здесь я приду

У меня была дискуссия о классах шрифтов в Scala с Дэниелом в Твиттере на днях, когда я неожиданно обнаружил один из моих незаконченных постов на ту же тему. Не то, что есть что-то новое, что вы узнаете из этой напыщенной речи. Но вот некоторые из моих мыслей о ценности, которую мышление на основе классов добавляет вашему пространству дизайна. Я начал этот пост, когда некоторое время назад опубликовал свои мысли об ортогональности в дизайне .

Давайте начнем с шаблона GOF Adapter .. объектного адаптера, который использует настоятельно рекомендованную технику композиции для связывания абстракций. Пример также тот же, который я использовал для обсуждения ортогональности в дизайне.

case class Address(no: Int, street: String, city: String, 
state: String, zip: String)

И мы хотим адаптировать это к интерфейсу LabelMaker .. т.е. мы хотели бы использовать Address в качестве LabelMaker ..

trait LabelMaker[T] {
def toLabel(value: T): String
}

адаптер, который выполняет преобразование интерфейса ..

// adapter class
case class AddressLabelMaker extends LabelMaker[Address] {
def toLabel(address: Address) = {
import address._
"%d %s, %s, %s - %s".format(no, street, city, state, zip)
}
}

// the adapter provides the interface of the LabelMaker on an Address
AddressLabelMaker().toLabel(Address(100, "Monroe Street", "Denver", "CO", "80231"))

Теперь, какова непредвиденная сложность, которую мы вводим в вышеупомянутый дизайн?

В качестве клиента наша точка внимания сместилась на класс адаптера AddressLabelMaker, который оборачивает нашу оригинальную абстракцию, экземпляр Address. Это приводит к кризису идентичности самого экземпляра Address, типичной проблеме для любой идиомы, основанной на оболочке. Идентификационные данные адаптера теперь теряются в идентификационных данных адаптера. И если вы достаточно смелы, чтобы иметь адаптивного члена в качестве совокупного члена другого объекта, то, очевидно, вся структура адаптера нарушается.

Вывод: объектные адаптеры не составляют. И классовый вариант адаптера pattren хуже в том смысле, что у вас есть проводка по наследству с самого начала, что делает всю конструкцию намного более жесткой и связанной.

Введите тип класса.

В приведенной выше структуре шаблона адаптера адаптер был включен в адаптер, что привело к потере идентификатора адаптера, который мы обсуждали ранее. Давайте рассмотрим другой подход и посмотрим, сможем ли мы абстрагировать адаптер от использования клиента. Подход класса типов действительно предлагает этот способ составления абстракций — система типов языка выбирает соответствующий экземпляр адаптера во время компиляции, так что пользователь может явно программировать для адаптера.

Рассмотрим эту функцию printLabel, которая принимает аргумент и печатает метку, используя LabelMaker, который мы ей предоставляем.

def printLabel[T](t: T)(lm: LabelMaker[T]) = lm.toLabel(t)

Чтобы сделать метку из адреса, нам нужно определить адаптер, который это делает. В Scala есть поддержка модулей первого класса через синтаксис объекта. Давайте определим модуль, который определяет семантику для преобразования адреса в LabelMaker.

object LabelMaker {
implicit object AddressLabelMaker extends LabelMaker[Address] {
def toLabel(address: Address): String = {
import address._
"%d %s, %s, %s - %s".format(no, street, city, state, zip)
}
}
}

Обратите внимание, что мы делаем адаптер объектом Scala с неявным квалификатором. Это делает то, что компилятор предоставляет неявный аргумент, если он находит один подходящий экземпляр в лексической области видимости. Но для этого нам нужен аргумент LabelMaker, чтобы printLabel также неявно.

def printLabel[T](t: T)(implicit lm: LabelMaker[T]) = lm.toLabel(t)

или используя синтаксис , связанный с контекстом Scala 2.8 , мы можем сделать неявный аргумент безымянным.

def printLabel[T: LabelMaker](t: T) = implicitly[LabelMaker[T]].toLabel(t)

Мы вообще не предоставляем неявный аргумент, вместо этого используем синтаксис с привязкой к контексту, где компилятор автоматически выбирает соответствующий экземпляр из окружающей лексической области видимости. В приведенном выше примере неявный объект AddressLabelMaker должен находиться в области действия функции, в которой вы вызываете printLabel. Он будет жаловаться, если не найдет его — следовательно, вы будете предупреждены во время самой компиляции. Смотри, ма — никаких злых сбоев во время выполнения …

Теперь, если мы хотим напечатать ярлык для адреса, мы делаем

printLabel(Address(100, "Monroe Street", "Denver", "CO", "80231"))

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

Давайте посмотрим, как это делает Haskell.

Мы зашли так далеко, обсуждая классы типов, не упоминая один раз Haskell. Давайте внесем коррективы и посмотрим, как описанный выше дизайн вписывается в чисто функциональный мир Haskell.

LabelMaker — это класс типов — давайте определим его в Haskell.

class LabelMaker a where
toLabel :: a -> String

Это соответствует нашему определению черты LabelMaker в Scala.

Мы хотим сделать Address LabelMaker. Вот пример вышеуказанного класса типов для Address.

instance LabelMaker Address where
toLabel (Address h s c st z) = show(h) ++ " " ++ s ++ "," ++ c ++ "," ++ st ++ "-" ++ z

Это соответствует неявному определению AddressLabelMaker объекта в Scala, которое мы сделали выше. Первоклассная поддержка Scala для исполняемых модулей — это дополнительная функция, которой у нас нет в Haskell.

О, кстати, вот определение адреса в синтаксисе записи на Haskell.

type HouseNo = Int
type Street = String
type City = String
type State = String
type Zip = String

data Address = Address {
houseNo :: HouseNo
, street :: Street
, city :: City
, state :: State
, zip :: Zip
}

И теперь мы можем определить printLabel, который использует класс типа и генерирует строку для метки.

printLabel :: (LabelMaker a) => a -> String
printLabel a = toLabel(a)

Это соответствует Scala-определению printLabel, которое мы имеем выше.

Небольшое наблюдение ..

Обратите внимание, что одно место, где Haskell гораздо менее многословен, чем Scala в приведенной выше реализации, — это определение экземпляра класса типа. Но здесь добавленное многословие в Scala не лишено цели и предлагает определенное преимущество перед соответствующим определением Haskell. В случае Scala мы называем экземпляр явно как AddressLabelMaker, в то время как экземпляр в случае Haskell не называется. Компилятор Haskell просматривает словарь в глобальном пространстве имен для поиска подходящего экземпляра. В случае Scala поиск выполняется локально в объеме вызова метода, который его инициировал. И так как это экземпляр с явным именем в Scala, вы можете добавить другой экземпляр в область видимости, и он будет выбран для неявного использования.Рассмотрим приведенный выше пример, где у нас есть другой экземпляр класса типов для Address, который генерирует метку особым образом.

object SpecialLabelMaker {
implicit object AddressLabelMaker extends LabelMaker[Address] {
def toLabel(address: Address): String = {
import address._
"[%d %s, %s, %s - %s]".format(no, street, city, state, zip)
}
}
}

Мы можем использовать этот специальный экземпляр вместо области по умолчанию и создать метку для нашего адреса совершенно особым образом.

import SpecialLabelMaker._
printLabel(Address(100, "Monroe Street", "Denver", "CO", "80231"))

Вы не получаете эту функцию в Haskell.

Классы типов в Scala и Haskell.

Классы типов определяют набор контрактов, которые необходимо реализовать типу Adaptee. Многие люди неправильно интерпретируют классы типов как интерфейсы в Java или других языках программирования. В интерфейсах основное внимание уделяется полиморфизму подтипа, а в классах типов фокус меняется на параметрический полиморфизм. Вы реализуете контракты, которые класс типов публикует для несвязанных типов.

Реализация класса типов имеет два аспекта:

  1. определение контрактов, которые необходимо выполнить
  2. разрешить выбор соответствующего экземпляра на основе статической проверки типа, предлагаемой языком

Haskell использует синтаксис класса для реализации (1), хотя этот класс является совершенно другим понятием, чем тот, который мы ассоциируем с программированием ОО. Для Scala мы использовали черты, чтобы реализовать эту функцию и расширение черты в объект для реализации его экземпляров.

Как я упоминал ранее, Haskell использует глобальный словарь для реализации (2), в то время как Scala делает это посредством поиска во включающей области действия вызова. Это дает дополнительную гибкость в Scala, где вы можете выбрать экземпляр, перенеся его в локальную область.

 

От http://debasishg.blogspot.com/2010/06/scala-implicits-type-classes-here-i.html