Подтвердить что ты не робот

Список иллюстраций: OOP бьет Haskell?

Я хочу создать список разных вещей, которые имеют одно свойство, а именно: они могут быть превращены в строку. Объектно-ориентированный подход прост: определить интерфейс Showable и сделать его интересными. Вторая точка может в принципе быть проблемой, если вы не можете изменить классы, но пусть притворяется, что это не так. Затем вы создаете список Showable и заполняете его объектами этих классов без каких-либо дополнительных помех (например, повышение уровня обычно выполняется неявно). Доказательство концепции в Java приведено здесь.

Мой вопрос в том, какие варианты для этого у меня есть в Haskell? Ниже я обсуждаю подходы, которые я пробовал и которые на самом деле не удовлетворяют меня.

Подход 1: существительные. Работает, но уродливо.

{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall a. Show a => Sh a

aList :: [Showable]
aList = [Sh (1 :: Int), Sh "abc"]

Основным недостатком для меня здесь является необходимость Sh при заполнении списка. Это очень похоже на upcast-операции, которые неявно выполняются на OO-языках.

В общем случае фиктивная обертка Showable для вещи, которая уже находится в языке - Show type class, добавляет дополнительный шум в мой код. Нехорошо.

Подход 2: impredicatives. Желательно, но не работает.

Самый простой тип для такого списка для меня и то, что я действительно желаю, будет:

{-# LANGUAGE ImpredicativeTypes #-}
aList :: [forall a. Show a => a]
aList = [(1 :: Int), "abc"]

Кроме того (как я слышал) ImpredicativeTypes "хрупка в лучшем случае и сломана в худшем случае" он не компилируется:

Couldn't match expected type ‘a’ with actual type ‘Int’
  ‘a’ is a rigid type variable bound by
      a type expected by the context: Show a => a

и ту же ошибку для "abc". (Обратите внимание на подпись типа для 1: без нее. Я получаю еще более странное сообщение: Could not deduce (Num a) arising from the literal ‘1’).

Подход 3: типы Rank-N вместе с некоторыми функциональными списками (списки различий?).

Вместо проблемного ImpredicativeTypes можно было бы предпочесть более стабильный и широко принятый RankNTypes. Это в основном означает: двигаться желаемый forall a. Show a => a из конструктора типа (т.е. []) для простых типов функций. Следовательно, нам нужно некоторое представление списков как простых функций. Как я едва слышал, есть такие представления. Тот, о котором я слышал, - это списки различий. Но в Dlist package основной тип - старый добрый data, поэтому мы возвращаемся к impredicatives. Я еще не исследовал эту строку, поскольку я подозреваю, что она может привести к более подробному коду, чем к подходу 1. Но если вы считаете, что это не так, пожалуйста, дайте мне пример.

Нижняя строка: как бы вы атаковали такую ​​задачу в Haskell? Не могли бы вы дать более сжатое решение, чем на языке OO (особенно вместо заполнения списка - см. Комментарий для кода в подходе 1)? Можете ли вы прокомментировать, насколько важны перечисленные выше подходы?

UPD (на основе первых комментариев): этот вопрос, конечно, упрощается с целью удобочитаемости. Реальная проблема заключается в том, как хранить вещи, которые имеют один и тот же тип, например, могут обрабатываться позже несколькими способами (Show имеет только один метод, но другие классы могут иметь более одного). Это приводит к устранению решений, которые предлагают применить метод Show прямо при заполнении списка.

4b9b3361

Ответ 1

Решения HList -style будут работать, но можно уменьшить сложность, если вам нужно работать только со списками ограниченных экзистенций, и вам не нужен другой механизм HList.

Вот как я справляюсь с этим в своем existentialist пакете:

{-# LANGUAGE ConstraintKinds, ExistentialQuantification, RankNTypes #-}

data ConstrList c = forall a. c a => a :> ConstrList c
                  | Nil
infixr :>

constrMap :: (forall a. c a => a -> b) -> ConstrList c -> [b]
constrMap f (x :> xs) = f x : constrMap f xs
constrMap f Nil       = []

Затем это можно использовать следующим образом:

example :: [String]
example
  = constrMap show
              (( 'a'
              :> True
              :> ()
              :> Nil) :: ConstrList Show)

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

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

Кроме того, если (как в случае с Show) существует только один метод класса, подход, который я бы рекомендовал, применил бы этот метод к каждому элементу в списке непосредственно, как в ответе ErikR, или о первом методе в phadej ответ.

Похоже, что актуальная проблема сложнее, чем просто список значений Show -able, поэтому трудно дать определенную рекомендацию, какая из них конкретно является наиболее подходящей без более конкретной информации.

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

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

Это может быть обобщено на более высокие типы, такие как:

data AnyList c f = forall a. c a => f a :| (AnyList c f)
                 | Nil
infixr :|

anyMap :: (forall a. c a => f a -> b) -> AnyList c f -> [b]
anyMap g (x :| xs) = g x : anyMap g xs
anyMap g Nil       = []

Используя это, мы можем (например) создать список функций, имеющих Show -возможные типы результатов.

example2 :: Int -> [String]
example2 x = anyMap (\m -> show (m x))
                    (( f
                    :| g
                    :| h
                    :| Nil) :: AnyList Show ((->) Int))
  where
    f :: Int -> String
    f = show

    g :: Int -> Bool
    g = (< 3)

    h :: Int -> ()
    h _ = ()

Мы видим, что это истинное обобщение, определяя:

type ConstrList c = AnyList c Identity

(>:) :: forall c a. c a => a -> AnyList c Identity -> AnyList c Identity
x >: xs  = Identity x :| xs
infixr >:

constrMap :: (forall a. c a => a -> b) -> AnyList c Identity -> [b]
constrMap f (Identity x :| xs) = f x : constrMap f xs
constrMap f Nil                = []

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

Ответ 2

Так как оценка является ленивой в Haskell, как насчет просто создания списка фактических строк?

showables = [ show 1, show "blah", show 3.14 ]

Ответ 3

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

{-# LANGUAGE PolyKinds, KindSignatures, GADTs, TypeFamilies
   , TypeOperators, DataKinds, ConstraintKinds, RankNTypes, PatternSynonyms  #-} 

import Data.List (intercalate)
import GHC.Prim (Constraint)

infixr 5 :&
data HList xs where 
  None :: HList '[] 
  (:&) :: a -> HList bs -> HList (a ': bs) 

-- | Constraint All c xs holds if c holds for all x in xs
type family All (c :: k -> Constraint) xs :: Constraint where 
  All c '[] = () 
  All c (x ': xs) = (c x, All c xs) 

-- | The list whose element types are unknown, but known to satisfy
--   a class predicate. 
data CList c where CL :: All c xs => HList xs -> CList c  

cons :: c a => a -> CList c -> CList c
cons a (CL xs) = CL (a :& xs) 

empty :: CList c 
empty = CL None 

uncons :: (forall a . c a => a -> CList c -> r) -> r -> CList c -> r 
uncons _ n (CL None) = n 
uncons c n (CL (x :& xs)) = c x (CL xs) 

foldrC :: (forall a . c a => a -> r -> r) -> r -> CList c -> r 
foldrC f z = go where go = uncons (\x -> f x . go) z 

showAll :: CList Show -> String 
showAll l = "[" ++ intercalate "," (foldrC (\x xs -> show x : xs) [] l) ++ "]" 

test = putStrLn $ showAll $ CL $ 
  1 :& 
  'a' :& 
  "foo" :& 
  [2.3, 2.5 .. 3] :& 
  None 

Ответ 4

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

infixr 5 <:

(<:) :: Show a => a -> [String] -> [String]
x <: l = show x : l

Итак, вы можете сделать:

λ > (1 :: Int) <: True <: "abs" <: []
["1","True","\"abs\""]

Это не [1 :: Int, True, "abs"], но не намного дольше.

К сожалению, вы не можете перестроить синтаксис [...] с помощью RebindableSyntax.


Другим подходом является использование HList и сохранение информации типа все, т.е. нисходящих преобразований, а не переходов:

{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE PolyKinds             #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}

import GHC.Exts (Constraint)

infixr 5 :::

type family All (c :: k -> Constraint) (xs :: [k]) :: Constraint where
  All c '[]       = ()
  All c (x ': xs) = (c x, All c xs)

data HList as where
  HNil :: HList '[]
  (:::) :: a -> HList as -> HList (a ': as)

instance All Show as => Show (HList as) where
  showsPrec d HNil       = showString "HNil"
  showsPrec d (x ::: xs) = showParen (d > 5) (showsPrec 5 x)
                         . showString " ::: "
                         . showParen (d > 5) (showsPrec 5 xs)

И после всего этого:

λ *Main > (1 :: Int) ::: True ::: "foo" ::: HNil
1 ::: True ::: "foo" ::: HNil

λ *Main > :t (1 :: Int) ::: True ::: "foo" ::: HNil
(1 :: Int) ::: True ::: "foo" ::: HNil
  :: HList '[Int, Bool, [Char]]

Существуют различные способы кодирования гетерогенного списка, HList - это один, есть generics-sop с NP I xs. Это зависит от того, чего вы пытаетесь достичь в более широком контексте, если это такой подход "все-все-типы" - это то, что вам нужно.

Ответ 5

Я бы сделал что-то вроде этого:

newtype Strings = Strings { getStrings :: [String] }

newtype DiffList a = DiffList { getDiffList :: [a] -> [a] }

instance Monoid (DiffList a) where
    mempty                          = DiffList id
    DiffList f `mappend` DiffList g = DiffList (f . g)

class ShowList a where
    showList' :: DiffList String -> a

instance ShowList Strings where
    showList' (DiffList xs) = Strings (xs [])

instance (Show a, ShowList b) => ShowList (a -> b) where
    showList' xs x = showList' $ xs `mappend` DiffList (show x :)

showList = showList' mempty

Теперь вы можете создать ShowList следующим образом:

myShowList = showList 1 "blah" 3.14

Вы можете вернуть список строк с помощью getStrings следующим образом:

myStrings = getStrings myShowList

Вот что происходит:

  • Значение типа ShowList a => a может быть:

    • Либо список строк, завернутых в обертку Strings newtype.
    • Или функция из экземпляра Show в экземпляр ShowList.
  • Это означает, что функция ShowList представляет собой переменную функцию аргумента, которая принимает произвольное количество печатаемых значений и в конечном итоге возвращает список строк, завернутых в обертку Strings newtype.

  • В конечном итоге вы можете вызвать getStrings по значению типа ShowList a => a, чтобы получить окончательный результат. Кроме того, вам не нужно делать какое-либо явное принуждение типа.

Преимущества:

  • Вы можете добавлять новые элементы в свой список, когда захотите.
  • Синтаксис является кратким. Вам не нужно вручную добавлять Show перед каждым элементом.
  • Он не использует никаких языковых расширений. Следовательно, он также работает в Haskell 98.
  • Вы получаете лучшее из обоих миров, тип безопасности и отличный синтаксис.
  • Используя списки различий, вы можете построить результат в линейном времени.

Для получения дополнительной информации о функциях с переменными аргументами прочитайте ответ на следующий вопрос:

Как работает Haskell printf?

Ответ 6

Мой ответ в основном такой же, как у ErikR: тип, который наилучшим образом воплощает ваши требования, [String]. Но я пойду немного в логику, которая, как мне кажется, оправдывает этот ответ. Ключ в этой цитате из вопроса:

[...] вещи, которые имеют одно свойство, а именно, они могут быть превращены в строку.

Позвольте называть этот тип Stringable. Но теперь ключевым наблюдением является следующее:

То есть, если ваше утверждение выше - вся спецификация типа Stringable, то есть пара-функции с этими сигнатурами:

toString :: Stringable -> String
toStringable :: String -> Stringable

..., так что обе функции являются обратными. Когда два типа изоморфны, любая программа, использующая любой из типов, может быть переписана в терминах другой без каких-либо изменений в ее семантике. Поэтому Stringable не позволяет вам делать что-либо, что String уже не позволяет вам делать это!

Более конкретно, дело в том, что этот рефакторинг гарантированно работает независимо от того, что:

  • В каждой точке вашей программы, где вы превращаете объект в Stringable и вставляете его в [Stringable], превратите объект в String и вставьте его в [String].
  • В каждой точке вашей программы, которую вы потребляете Stringable, применяя к ней toString, теперь вы можете исключить вызов toString.

Обратите внимание, что этот аргумент обобщает типы, более сложные, чем Stringable, со многими "методами". Так, например, тип "вещей, которые вы можете превратить в String или Int", изоморфен (String, Int). Тип "вещей, которые вы можете либо превратить в String, либо объединить их с Foo для создания Bar", изоморфен (String, Foo -> Bar). И так далее. В принципе, эта логика приводит к кодировке "записи методов", которая вызвала другие ответы.

Я думаю, что урок, который можно извлечь из этого, заключается в следующем: вам нужна спецификация богаче, чем просто "можно превратить в строку", чтобы оправдать использование любого из механизмов, которые вы подняли. Так, например, если мы добавим требование, чтобы значения Stringable могли быть downcast исходным типом, теперь возможно существование экзистенциального типа:

{-# LANGUAGE GADTs #-}

import Data.Typeable

data Showable = Showable
    Showable :: (Show a, Typeable a) => a -> Stringable

downcast :: Typeable a => Showable -> Maybe a
downcast (Showable a) = cast a

Этот тип Showable не изоморфен String, потому что ограничение Typeable позволяет нам реализовать функцию downcast, которая позволяет различать разные Showable, которые производят одну и ту же строку. Более богатую версию этой идеи можно увидеть в этом "примере формы" Gist.

Ответ 7

Вы можете хранить частично примененные функции в списке.

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

data Sphere = ...
data Triangle = ...

data Ray = ...
data IntersectionResult = ...

class Intersect t where
      intersect :: t -> Ray -> Maybe IntersectionResult

instance Intersect Sphere where ...
instance Intersect Triangle where ...

Теперь мы можем частично применить intersect, чтобы получить список Ray -> Maybe IntersectionResult, например:

myList :: [(Ray -> Maybe IntersectionResult)]
myList = [intersect sphere, intersect triangle, ...]

Теперь, если вы хотите получить все пересечения, вы можете написать:

map ($ ray) myList -- or map (\f -> f ray) myList

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

class ShapeWithSomething t where
        getSomething :: t -> OtherParam -> Float

data ShapeIntersectAndSomething = ShapeIntersectAndSomething {
          intersect :: Ray -> Maybe IntersectionResult,
          getSomething :: OtherParam -> Float}

Что-то я не знаю, это накладные расходы на этот подход. Нам нужно сохранить указатель на функцию и указатель на форму, и это для каждой функции интерфейса, что много по сравнению с общим vtable, обычно используемым на языке OO. Я не знаю, может ли GHC оптимизировать это.

Ответ 8

Ядро проблемы заключается в следующем: вы хотите отправить (читать select, какую функцию вызывать) во время выполнения, в зависимости от того, что такое "тип" объекта. В Haskell это может быть достигнуто путем переноса данных в тип данных суммы (который называется здесь ShowableInterface):

data ShowableInterface = ShowInt Int | ShowApple Apple | ShowBusiness Business

instance Show ShowableInterface where
   show (ShowInt i)      = show i
   show (ShowApple a)    = show a
   show (ShowBusiness b) = show b  

list=[ShowInt 2, ShowApple CrunchyGold, ShowBusiness MoulinRouge]

show list

будет соответствовать чему-то подобному в Java:

class Int implements ShowableInterface
{
   public show {return Integer.asString(i)};
}
class Apple implements ShowableInterface
{
   public show {return this.name};
}
class ShowBusiness implements ShowableInterface
{
   public show {return this.fancyName};
}

List list = new ArrayList (new Apple("CrunchyGold"), 
                           new ShowBusiness("MoulingRouge"), new Integer(2));

поэтому в Haskell вам нужно явно переносить материал в ShowableInterface, в Java этот перенос выполняется неявно при создании объекта.

кредит идет на #haskell IRC для объяснения этого мне год назад или около того.