Составление функций с промежуточным полиморфным типом в Haskell

У меня есть следующий файл:

module SimpleComposition where

class Intermediate a where
    f :: a -> Int
    g :: Char -> a

h :: Char -> Int
h = f . g

При попытке загрузить его в ghci я получаю сообщение об ошибке:

main.hs:8:5: error:
    * No instance for (Intermediate a0) arising from a use of `f'
    * In the first argument of `(.)', namely `f'
      In the expression: f . g
      In an equation for `h': h = f . g
  |
8 | h = f . g
  |     ^

Я считаю, что проблема в том, что кто-то может использовать 2 разных типа, которые являются экземплярами Intermediate в этой композиции. Как я могу гарантировать, что это то же самое, когда я экспортирую этот модуль?

PS: Это лучший минимальный пример моей проблемы, чем вопрос, который я задавал ранее (Как компоновать полиморфные функции в Haskell?).

Проблема в том, что Haskell не знает, что a использовать в качестве промежуточного.

Willem Van Onsem 19.12.2020 22:02

Ответы дают решения ошибки, но я думаю, что они не объясняют проблему, вытекающую из ошибки. Итак, позвольте мне попробовать это в качестве дополнения к исправлениям. Предположим, я пишу instance Intermediate () where {f _ = 0; g _ = ();}; instance Intermediate Bool where {f _ = 1; g _ = False;}. Какое поведение вы ожидаете от h там?

Daniel Wagner 19.12.2020 22:40
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
121
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Проблема не в том, что экземпляр нельзя вывести, а в том, что у компилятора действительно нет возможности узнать, какой тип вам может понадобиться. g может производить любой запрашиваемый тип (при условии, что у него есть экземпляр Intermediate), f может потреблять любой такой тип... но никто не указывает, какой именно.

Но это легко исправить: просто выберите тип сейчас. Конечно, это должен быть тот, у которого есть экземпляр; например если у вас есть

instance Intermediate Char where
  f = fromEnum
  g = id

тогда вы можете использовать

h :: Char -> Int
h = (f :: Char -> Int) . g

Более лаконичный способ исправить выбор типа — использовать синтаксическое расширение:

{-# LANGUAGE TypeApplications #-}

h = f @Char . g

... или, чтобы подчеркнуть, что вы просто исправляете шрифт посередине,

h = f . id @Char . g
Ответ принят как подходящий

Я считаю, что проблема в том, что кто-то может использовать 2 разных типа, которые являются экземплярами Intermediate в этой композиции.

Нет, проблема в том, что Haskell больше не может извлечь из подписи, что a использовать. Представьте, что есть два типа Intermediate:

instance Intermediate Char where
    # …

instance Intermediate Bool where
    # …

теперь есть две реализации для h:

h :: Char -> Int
h = f . (g :: Char -> Char)

или:

h :: Char -> Int
h = f . (g :: Char -> Bool)

может быть бесконечное количество типов Intermediate, которые можно использовать. Проблема в том, что Haskell не может сказать, основываясь на сигнатуре типа, какой тип использовать.

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

Простой способ исправить это — использовать asTypeOf :: a -> a -> a. По сути, это функция const, но два параметра имеют один и тот же тип. Таким образом, это используется для добавления подсказки, какой тип использовать, например:

h :: Intermediate a => a -> Char -> Int
h a x = f (g x `asTypeOf` a)

Таким образом, здесь значение параметра a не имеет никакого значения, это способ «внедрить» тип, который будет использоваться в качестве типа для результата g и параметра f.

Если вы таким образом позже используете h, вы можете работать с:

h (undefined :: Char) 'a'

чтобы указать, что f должен иметь тип Char -> Char, а g должен иметь тип Char -> Int.

Как говорят @leftroundabout и @DanielWagner, более чистое решение без использования такой фиктивной переменной — добавить переменную типа в подпись:

{-# LANGUAGE AllowAmbiguousTypes, ScopedTypeVariables, TypeApplications #-}

h :: forall a. Intermediate a => Char -> Int
h = f . g @ a

тогда мы можем использовать h с переменной типа с:

h @ Char 'a'

Мне не нравится предложение передать фиктивный параметр. По крайней мере, упомяните версию ScopedTypeVariables / AllowAmbiguousTypes (или версию с прокси, если хотите, хотя прокси IMO тоже устарели).

leftaroundabout 19.12.2020 22:32

@leftaroundabout: я не уверен AllowAmbiguousTypes здесь можно решить проблему? Идея заключается в том, чтобы разрешить «доступ» к промежуточному типу, когда мы используем h как функцию, а не когда мы ее определяем. Таким образом, человек позже может решить, проходят ли данные «через» Char или Bool, а не решать, где мы определяем функцию h.

Willem Van Onsem 19.12.2020 22:35

@WillemVanOnsem Да, AllowAmbiguousTypes решает эту проблему. (На самом деле, это именно та проблема, для решения которой он был разработан.) Написание h :: Intermediate a => Char -> Int позволяет звонящему сказать h @String (или что-то еще).

Daniel Wagner 19.12.2020 22:37

@DanielWagner: ну, я попробовал это и получил Could not deduce (Intermediate a0) arising from a use of ‘f’ from the context: Intermediate a, я пробовал это с h = f . id @a . g и использовал ScopedTypeVariables, TypeApplications, но, вероятно, из-за какой-то заморозки, у меня это не работает: repl.it/@willemvo/ неоднозначный#main.hs

Willem Van Onsem 19.12.2020 22:51

@WillemVanOnsem, ты забыл ∀ a. (Такая заморозка мозгов со мной бы никогда не случилась...).

leftaroundabout 19.12.2020 23:15

Лучшей версией подхода с фиктивным аргументом является подход с использованием прокси-аргумента. asProxyTypeOf :: a -> proxy a -> a; asProxyTypeOf a _ = a. Импортируя Data.Proxy, вы можете написать a `asProxyTypeOf` (Proxy :: Proxy Int) или подобное.

dfeuer 20.12.2020 01:23

Другие вопросы по теме