Продолжим тему полиморфизма. Как вы, возможно, уже знаете из предыдущей статьи, которую я написал о параметрическом полиморфизме, существует три типа полиморфизма:

  • полиморфизм подтипа — старый добрый ООП (Cat — подтип Animal)
  • параметрический полиморфизм — дженерики. Когда у вас есть тип, который принимает тип, например. Glass[Liquid]
  • Специальный полиморфизм — это что-то вроде дженериков, но более гибкое и мощное. Давайте поговорим об этом сегодня.

Если вы знакомы с экосистемами Scala (или других языков функционального программирования), вы наверняка сталкивались с классами типов. Может быть, вы искали в Интернете, читали статью о них, и теперь у вас есть четкое представление о том, что они из себя представляют. Если это так, то это может оказаться скучным чтением для вас.

Однако если бы вы были похожи на меня в свое время и после того, как набрали «что такое классы типов в scala» в первый раз, вы потратили бы час, чувствуя, что вы на самом деле не понимаете ближе к их пониманию — эта статья может быть как раз для вас.

Отказ от ответственности:

Я предполагаю, что вы читали последнюю статью о параметрическом полиморфизме, которую я написал — я буду ссылаться на нее здесь, и знакомство с ней сейчас будет очень полезно. Я также предполагаю, что вы знаете о имплицитах. О них я тоже писал здесь.

Что именно означает специальный полиморфизм?

Сначала давайте рассмотрим определение того, что такое ad hoc. Что-то, что делается в специальной манере, это:

делается только тогда, когда это необходимо или необходимо

or

созданные или сделанные для определенной цели по мере необходимости

Когда я говорю, что делаю что-то специально, я имею в виду, что делаю это по мере необходимости, я не пытаюсь встроить это в существующий рабочий процесс. /модель.

Если мы подумаем о «я не пытаюсь планировать это в существующем рабочем процессе» в контексте полиморфизма, мы можем прийти к « Я достигаю полиморфизма, но я не модифицирую существующий тип».

Таким образом, специальный полиморфизм (он же классы типов) заключается в возможности обогащать/расширятьсуществующий тип. с новым поведением без изменения кода данного типа. Конкретно это означает написание кода, такого как…

val str: String = "Hi there!"
val person: Person = Person(20, "Edith")

str.asFancyLogMessage    // <- this method doesn't exist in String code! 
person.asFancyLogMessage // <- this method doesn't exist in Person code!

…не касаясь исходного кода типов String или Person.

Это кажется очень… функциональным

Один из моих любимых моментов «а-а-а» при изучении классов типов был, когда я понял, что ad hoc полиморфизм, похоже, довольно хорошо вписывается в идею функционального программирования. Позвольте мне рассказать вам, почему.

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

В специальном полиморфизме мы полностью меняем его: мы фокусируемся на функции, которую некоторые типы могут применять. Как бы говоря: полет — это способность, которой обладает птица. Сон — это способность, которой обладают как птица, так и человек. Вы замечаете этот переключатель и то, как мы теперь пытаемся понять и описать мир с помощью функций?

Как написать класс типов?

Класс типов следует рассматривать как концепцию или шаблон,не столько как функциональность или существующую модель. Это означает, что для написания класса типов нам нужно несколько вещей:

  • общая черта, которая будет представлять, какую именно функцию мы хотим предоставить, например. LoggableString[A]. Здесь нам все равно, какие типы будут использовать наш класс типов. Мы заботимся только об описании функциональности.
trait LoggableString[A] {
    def asLogString(a: A): String
}
  • экземпляры класса типов — здесь мы начинаем заботиться о типах, которые будут использовать наш LoggableString. Если, например, мы хотим, чтобы Int мог применять функциональность, которую предлагает LoggableString, нам нужен экземпляр класса типов для Int: LoggableString[Int]. В этом случае нам нужно объяснить, что LoggableString и, более конкретно, def asLogString означают именно для Int. Это, конечно, относится к любому другому типу, который хочет быть обогащен функциональностью из LoggableString.
object LoggableStringInstances { // a set of type class instances

    // type class instance for Int
    implicit val intLoggableString = new LoggableString[Int] {
        def asLogString(int: Int): String = {
            s"Int value: ${int}"
        }
    }

    // type class instance for Person
    implicit val personLoggableString = new LoggableString[Person] {
        def asLogString(person: Person): String = {
            s"Person age: ${person.age}, person height: ${person.height}"
        }
    }

    // ...instances go on if needed
}
  • [необязательно] неявное преобразование, чтобы иметь возможность использовать красивый синтаксис, такой как person.asLogString вместо простого asLogString(person) — я пропущу детали, но не хочу слишком много утруждать себя сегодня.
  • использование: просто import LoggableStringInstances.personLoggableString и вы готовы использовать новый asLogString для Person.

Важное примечание! Код в этом разделе будет выглядеть совсем по-другому в Scala 3, так как были введены новые ключевые слова и функциональные возможности, поэтому имейте в виду, что синтаксис в Scala 3 будет другим — однако основная концепция остается прежней. эм>

В качестве бонуса и вот вам наглядная заметка к шрифтовым классам из моего личного блокнота:

Зачем писать класс типов?

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

Но с большой силой приходит большая ответственность, и вы, скорее всего, увидите классы типов, используемые для добавления странных функций расширения здесь или там. Лично я любой ценой избегаю классов типов при написании обычного повседневного кода. Чем больше классов типов используется в коде, тем сложнее его обосновать. Если они используются широко и без четкого намерения и подходящего варианта использования, они предполагают, что могут быть некоторые проблемы с дизайном, которые были заметены под ковер. Золотое правило: если вы не уверены, что ваш вариант использования является кандидатом для класса типов, это, скорее всего, означает, что это не так.

Почему тогда они существуют?

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

Еда на вынос

Вот список выводов из сегодняшней статьи:

  • Классы типов — это способ достижения специального полиморфизма не только в Scala, но и в других языках функционального программирования. Это концепция, а не функциональность.
  • «ad hoc» в контексте полиморфизма означает отсутствие каких-либо подтипов, а это означает: не касаться кода типа, который мы хотим расширить.
  • классов типов лучше избегать в повседневном программировании. Они позволяют взглянуть на мир с высоты птичьего полета и используются в некоторых библиотеках, ориентированных на функциональное моделирование мира.

Спасибо, что дочитали до конца! Не стесняйтесь, дайте мне знать, если есть какие-либо концепции или темы, которые вы хотели бы, чтобы я осветил в будущих статьях — увидимся в следующий раз!