Два полиморфа c классов в одной функции - PullRequest
3 голосов
/ 22 марта 2020

У меня есть этот код с монадами состояний:

import Control.Monad.State

data ModelData = ModelData String
data ClientData = ClientData String

act :: String -> State ClientData a -> State ModelData a
act _ action = do
  let (result, _) = runState action $ ClientData ""
  return result

addServer :: String -> State ClientData ()
addServer _ = return ()

scenario1 :: State ModelData ()
scenario1 = do
  act "Alice" $ addServer "https://example.com"

Я пытаюсь обобщить его с помощью polymorphi c классов типов, используя следующий подход: https://serokell.io/blog/tagless-final.

Я могу обобщить ModelData:

import Control.Monad.State

class Monad m => Model m where
  act :: String -> State c a -> m a

data Client = Client String

addServer :: String -> State Client ()
addServer _ = return ()

scenario1 :: Model m => m ()
scenario1 = do
  act "Alice" $ addServer "https://example.com"

Но когда я пытаюсь сделать это как с ModelData, так и с ClientData, он не может скомпилировать:

module ExampleFailing where

class Monad m => Model m where
  act :: Client c => String -> c a -> m a

class Monad c => Client c where
  addServer :: String -> c ()

scenario1 :: Model m => m ()
scenario1 = do
  act "Alice" $ addServer "https://example.com"

Ошибка:

    • Could not deduce (Client c0) arising from a use of ‘act’
      from the context: Model m
        bound by the type signature for:
                   scenario1 :: forall (m :: * -> *). Model m => m ()
        at src/ExampleFailing.hs:9:1-28
      The type variable ‘c0’ is ambiguous
    • In the expression: act "Alice"
      In a stmt of a 'do' block:
        act "Alice" $ addServer "https://example.com"
      In the expression:
        do act "Alice" $ addServer "https://example.com"
   |
11 |   act "Alice" $ addServer "https://example.com"
   |   ^^^^^^^^^^^

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

{-# LANGUAGE MultiParamTypeClasses #-}

module ExamplePassing where

class Monad m => Model m c where
  act :: Client c => String -> c a -> m (c a)

class Monad c => Client c where
  addServer :: String -> c ()

scenario1 :: (Client c, Model m c) => m (c ())
scenario1 = do
  act "Alice" $ addServer "https://example.com"

Буду очень признателен за ваш совет. Спасибо!

1 Ответ

4 голосов
/ 22 марта 2020

Ваша попытка обобщения с act :: Client c => String -> c a -> m a технически верна: это буквально перевод исходного кода, но замена State ModelData на m и State ClientData на c.

Произошла ошибка поскольку теперь, когда «клиентом» может быть что угодно, вызывающий scenario1 не может указать, каким он должен быть.

Видите, чтобы определить, какую версию addServer вызывать, компилятор должен знать, что такое c, но из этого некуда сделать вывод! c не появляется ни в параметрах функции, ни в типе возврата. Так что технически это может быть абсолютно все, оно полностью скрыто внутри scenario1. Но «абсолютно все» недостаточно для компилятора, потому что выбор c определяет, какая версия addServer вызывается, что будет определять поведение программы.

Вот уменьшенная версия та же проблема:

f :: String -> String
f str = show (read str)

Это также не скомпилируется, потому что компилятор не знает, какие версии show и read вызывать.


У вас есть Несколько вариантов.

Первый , если scenario1 сам знает, какой клиент использовать, он может сказать это, используя TypeApplications:

scenario1 :: Model m => m ()
scenario1 = do
  act "Alice" $ addServer @(State ClientData) "https://example.com"

Во-вторых, scenario1 может переложить эту задачу на любого, кто ее вызывает. Для этого вам нужно объявить переменную generic c c, даже если она не указана ни в каких параметрах или аргументах. Это можно сделать с помощью ExplicitForAll:

scenario1 :: forall c m. (Client c, Model m) => m ()
scenario1 = do
  act "Alice" $ addServer @c "https://example.com"

(обратите внимание, что вам все равно нужно сделать @c, чтобы сообщить компилятору, какую версию addServer использовать; чтобы иметь возможность сделать это, вам понадобится ScopedTypeVariables, который включает ExplicitForAll)

Тогда потребителю придется сделать что-то вроде этого:

let server = scenario1 @(State ClientData)

Наконец, если для по какой-то причине вы не можете использовать TypeApplications, ExplicitForAll или ScopedTypeVariables, вы можете сделать то же самое для версии для бедного человека - использовать дополнительный фиктивный параметр, чтобы ввести переменную типа (это было сделано ранее times):

class Monad c => Client c where
  addServer :: Proxy c -> String -> c ()

scenario1 :: (Client c, Model m) => Proxy c -> m ()
scenario1 proxyC = do
  act "Alice" $ addServer proxyC "https://example.com"

(обратите внимание, что сам метод класса теперь также получил фиктивный параметр; в противном случае его снова не удастся вызвать)

Тогда потребителю придется сделай эту уродливую вещь:

let server = scenario1 (Proxy :: Proxy (State ClientData))
...