Конструкция совпадения Haskell аналогична шаблону типа F #?

TL;DR

Если я правильно понимаю, у Haskell нет подтипов так, как это делает F #. Таким образом, я ожидаю, что у него нет шаблона тестового типа для сопоставления, например F #. Имеет ли он какие-либо аналогичные конструкции, которые можно использовать для метапрограммирования?

Пример из практики

У меня есть проект F #, в котором у меня есть развязанные модули, которые говорят через механизм обмена сообщениями. Недавно я задавался вопросом, как будет выглядеть этот код, или если бы это было возможно написать так, если бы я должен был перенести его в Haskell.

Основная идея такова. Сообщение - это тип, который наследуется от интерфейса сообщения:

type Message = interface end

Обработчик - это функция, которая принимает определенный подтип Message и возвращает a Message:

type Handle<'TMsg when 'TMsg :> Message> = 'TMsg -> Message

Существует Bus с методом publish, который распространяет сообщение на свои внутренние каналы. Существует один тип Channel<'TMsg> для каждого типа сообщений, и они динамически добавляются при регистрации обработчика. Шина публикует все сообщения для всех каналов, и если она неправильного типа, канал просто возвращает пустую последовательность:

type Channel<'TIn when 'TIn :> Message>(...) = ...
    interface Channel with
        member x.publish (message : Message) =
            match message with
            | :? 'TIn as msg ->
                Seq.map (fun handle -> handle msg) _handlers
                |> Seq.filter (fun msg -> msg <> noMessage)
            | _ -> Seq.empty

В конечном итоге то, что я здесь делаю, - это динамическое метапрограммирование, которое позволяет мне иметь строго типизированные сообщения, которые по-прежнему проходят через один и тот же механизм посередине. Я не такой, как Haskell, как F #, и я не могу понять, как это сделать в Haskell.

Я прав, что у Haskell нет конструкции match... with... :?... as...? Имеет ли он подобную конструкцию или другой подход к подобному метапрограммированию, с которым вы можете решить эту проблему? Есть какой-то механизм box/unbox?

Ответ 1

Извините, что ответил на ваш вопрос Mu, но я бы тоже не делал этого в F #.

Это Message интерфейс, что известно как Маркер интерфейс, и хотя это спорное, я считаю, что код запах на любом языке, который поддерживает аннотацию (атрибуты, в .NET).

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

Я бы не создал такую ​​систему сообщений, как на С#, потому что интерфейс Marker ничего не добавляет, кроме иллюзии, что сообщение каким-то образом "набрано".

Я бы не создал такую ​​систему сообщений, как в F #, потому что в F # есть более эффективная, безопасная, строго типизированная альтернатива:

type UserEvent =
    | UserCreated of UserCreated
    | EmailVerified of EmailVerified
    | EmailChanged of EmailChanged

Это четко указывает, что множество событий конечно и хорошо известно. При этом вы можете проверять время компиляции вместо проверки времени выполнения.

(см. здесь для полного примера.)

Такой Дискриминационный Союз легко перевести на Haskell:

data UserEvent = UserCreated UserCreatedData
               | EmailVerified EmailVerifiedData
               | EmailChanged EmailChangedData

Ответ 2

Haskell разработан таким образом, что типы могут быть полностью стерты во время выполнения. То есть, когда реализация сохраняет значение типа T в памяти, нет необходимости помечать его какой-либо меткой, обозначающей "это тип T".

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

f :: a -> a

должен возвращать свой аргумент или не завершаться. То есть, мы знаем, что f=id (если мы знаем, что это заканчивается).

Конкретно это означает, что нет способа написать что-то вроде

f :: a -> a
f x = if a == Int then x+1 else x

Если вы хотите что-то сделать, можно вручную добавить теги типа через класс Data.Typeable:

{-# LANGUAGE ScopedTypeVariables, GADTs, TypeOperators #-}
import Data.Typeable

f :: forall a . Typeable a => a -> a
f x = case eqT :: Maybe (a :~: Int) of
      Just Refl -> x+1
      Nothing   -> x

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

Итак, если вам действительно нужно выполнить проверки типа времени выполнения, используйте ограничение Typeable. Обратите внимание, что это, возможно, слишком много общего. Если вы знаете, что у вас есть небольшое количество типов, возможно, вы можете использовать тип суммы вместо этого и использовать простой шаблон для конструкторов для проверки типа:

data T = TInt Int
       | TChar Char

f :: T -> T
f (TInt i) = TInt (i+1)
f (TChar c) = TChar c