Совпадение с образцом по типу полиморфного параметра - альтернативы

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

choice :: a -> Int
choice (_ :: Int) = 0
choice (_ :: String) = 1
choice _ = 2

Однако мы можем легко исправить это, обернув нужные типы в разные конструкторы данных и используя их в сопоставлении с образцом:

data Choice a = IntChoice Int | StringChoice String | OtherChoice a

choice :: Choice a -> Int
choice (IntChoice _) = 0
choice (StringChoice _) = 1
choice (OtherChoice _) = 2

Вопрос. Знаете ли вы, как это обойти? Есть ли функция в Haskell2010, GHC или любом из расширений, которая позволяет мне использовать первый вариант (или что-то подобное)?


person mucaho    schedule 19.05.2015    source источник
comment
Data.Typeable позволяет запрашивать типы во время выполнения, но его не следует использовать легкомысленно.   -  person chi    schedule 19.05.2015


Ответы (2)


Вопрос: Знаете ли вы способ обойти это? Есть ли функция в Haskell2010, GHC или любом из расширений, которая позволяет мне использовать первый вариант (или что-то подобное)?

Нет, ни в Haskell 2010, ни в каком-либо расширении GHC нет функции, позволяющей написать функцию типа

choice :: a -> Int

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

Даже с помощью хаков для проверки внутреннего представления данных GHC во время выполнения невозможно отличить значение типа Int от значения, тип которого является новым типом Int: эти типы имеют идентичные представления во время выполнения.

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

  1. обычное значение, как в вашем примере data Choice a = IntChoice Int | StringChoice String | OtherChoice a; choice :: Choice a -> Int, или

  2. словарь класса типов, используя либо собственный класс, как в ответе Дэвида Янга, либо встроенный класс Typeable:

    choice :: Typeable a => a -> Int
    choice a
      | typeOf a == typeOf (undefined :: Int)    = 0
      | typeOf a == typeOf (undefined :: String) = 1
      | otherwise                                = 2
    
person Reid Barton    schedule 19.05.2015
comment
Спасибо за разъяснение альтернатив. невозможно отличить значение типа Int от значения, тип которого является новым типом Int: эти типы имеют идентичные представления во время выполнения — разве не для этого предназначен TypeSynonymInstances? (упоминается в ответе Дэвида). Не могли бы вы дать определение функции choice, используя класс типов Typeable? - person mucaho; 19.05.2015
comment
@mucaho TypeSynonymInstances специально предназначен для экземпляров класса типов. В частности, он позволяет вам создать экземпляр для синонима типа. Синоним типа — это тип, который обрабатывается системой типов так же, как и другой тип (в данном случае type String = [Char]). Даже с этим расширением и OverlappingInstances вы не можете создать экземпляр как String, так и [Char], поскольку они являются синонимами. newtypes, с другой стороны, обрабатываются системой типов как разные типы, но они имеют точно такое же представление во время выполнения. - person David Young; 20.05.2015

Это путает два разных вида полиморфизма. Вам нужен специальный полиморфизм, который реализуется через классы типов. Видом полиморфизма функции типа a -> Int является параметрический полиморфизм. При параметрическом полиморфизме одно определение функции для choice должно работать для любого возможного типа a. В данном случае это означает, что он не может на самом деле использовать значение типа a, так как он ничего о нем не знает, поэтому choice должна быть постоянной функцией, такой как choice _ = 3. На самом деле это дает вам очень серьезные гарантии того, что может делать функция, просто глядя на ее тип (это свойство называется параметричностью).

С классом типа вы можете реализовать свой пример как:

class ChoiceClass a where
  choice :: a -> Int

instance ChoiceClass Int where
  choice _ = 0

instance ChoiceClass String where
  choice _ = 1

instance ChoiceClass a where
  choice _ = 2

Теперь я должен указать, что этот подход к классу типов часто является неправильным, особенно когда кто-то, кто только начинает, хочет его использовать. Вы определенно не хотите этого делать, чтобы избежать простого типа, такого как тип Choice в вашем вопросе. Это может добавить много сложности, а разрешение экземпляра поначалу может сбивать с толку. Обратите внимание, что для того, чтобы решение класса типов заработало, необходимо было включить два расширения: FlexibleInstances и TypeSynonymInstances, поскольку String является синонимом [Char]. OverlappingInstances необходим еще и потому, что классы типов работают по предположению об "открытом мире" (это означает, что позже любой может прийти и добавить экземпляр для нового типа, и это необходимо учитывать). Это не обязательно плохо, но в данном случае это признак возрастающей сложности, вызванной использованием решения класса типов вместо гораздо более простого решения типа данных. OverlappingInstances, в частности, может затруднить обдумывание вещей и работу с ними.

person David Young    schedule 19.05.2015
comment
Вам также нужно OverlappingInstances, чтобы фактически использовать этот класс — я нахожу это расширение особенно сложным и трудным для полного понимания. Например. stackoverflow.com/questions/29504107/ - person chi; 19.05.2015
comment
@chi Спасибо, что подняли этот вопрос! На мгновение я подумал, что в этом нет необходимости, потому что choice 'a' работает без него (хотя изначально я так и думал). Однако без него невозможно использовать два других экземпляра. Я также считаю, что это расширение трудно понять, и я определенно думаю, что оно добавляет дополнительную сложность, которой следует избегать, если это возможно. Я обновил свой ответ, чтобы упомянуть об этом. - person David Young; 19.05.2015
comment
Спасибо за ценную информацию. Итак, вы предлагаете придерживаться конструкторов данных, если это возможно, верно? - person mucaho; 19.05.2015
comment
Относительно OverlappingInstances — взгляните на Safe расширение GHC — оно ограничивает те (все?) случаи, когда более специализированные экземпляры классов типов в ссылающемся модуле могут привести к ошибкам времени выполнения. - person mucaho; 19.05.2015
comment
@mucaho Это связано с взаимодействием между модулями и с тем, какие экземпляры могут быть экспортированы модулем. Это интересно, я не думал, что есть случай, когда вы можете предотвратить экспорт экземпляра. Это, вероятно, единственный раз, когда вы можете предотвратить это. Кроме того, да, я бы определенно предложил придерживаться конструкторов данных. Классы типов почти всегда используются, когда у вас есть очень общий интерфейс, который, вероятно, сможет быть расширен другими людьми за счет новых экземпляров. Хорошие примеры включают Monoid и Functor. - person David Young; 20.05.2015