Ограничение типа в объявлении типа

Известный пример типового уровня натуральных чисел:

data Zero
data Succ n

У меня вопрос о желательном ограничении при применении конструктора типов Succ. Например, если мы хотим сделать такое ограничение в определении функции, мы можем использовать класс и контекст, как в этом коде:

class Nat n where
  toInt :: n -> Int
instance Nat Zero where
  toInt _ = 0
instance (Nat n) => Nat (Succ n) where
  toInt _ = 1 + toInt (undefined :: n)

Невозможно использовать toInt (undefined :: Succ Int), все в порядке.

Но как реализовать аналогичные ограничения на конструкции уровня типов (возможно, с некоторыми расширенными расширениями типов)?

Например, я бы хотел разрешить использование конструктора типа Succ только с типом Zero и с чем-то вроде этого: Succ (Succ Zero), Succ (Succ (Succ Zero)) и так далее. Как избежать такого плохого примера во время компиляции:

type Test = Succ Int

(на данный момент нет ошибки компиляции)

P.S.: Мне интереснее как создать ограничение на объявление типа последнего образца

См. Что такое расширение DataKinds для Haskell? именно этот пример.

user11228628 26.06.2019 19:15
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
143
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В настоящее время мы используем расширение DataKinds:

{-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables #-}

-- N is a type, but is also a kind now
-- Zero, Succ Zero, ... are values, but are also type-level values of
-- kind N
data N = Zero | Succ N

-- (We could import Proxy the library, instead)
data Proxy (n :: N) = Proxy

-- Now n is restricted to kind N
class Nat (n :: N) where
  toInt :: proxy n -> Int

instance Nat Zero where
  toInt _ = 0
instance (Nat n) => Nat (Succ n) where
  toInt _ = 1 + toInt (undefined :: Proxy n)

Затем мы можем использовать toInt (Proxy :: Proxy (Succ Zero)). Вместо этого toInt (Proxy :: Proxy (Succ Int)) выдаст вид ошибки, как и хотелось.

Лично я бы также заменил прокси на более современные вещи, такие как AllowAmbiguousTypes и TypeApplications, чтобы удалить неиспользуемый аргумент.

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

data N = Zero | Succ N

-- Now n is restricted to kind N
class Nat (n :: N) where
  toInt :: Int

instance Nat Zero where
  toInt = 0
instance (Nat n) => Nat (Succ n) where
  toInt = 1 + toInt @n

Используйте это как toInt @(Succ Zero). Синтаксис toInt @n выбирает n в классе типов. Он не соответствует никакому значению, обмениваемому во время выполнения, а только аргументу уровня типа, существующему во время компиляции.


С использованием

type Foo = Succ Int

также ошибки, как хотелось:

    • Expected kind ‘N’, but ‘Int’ has kind ‘*’
    • In the first argument of ‘Succ’, namely ‘Int’
      In the type ‘Succ Int’
      In the type declaration for ‘Foo’

Спасибо; только что заметил еще один: Proxy в class Nat (n :: N) where toInt :: proxy n -> Int...

Will Ness 27.06.2019 12:56

@WillNess Нет, на самом деле это стандартная идиома (которая мне не нравится, но я соглашусь). Использование переменной типа proxy вместо типа Proxy позволяет использовать и другие типы прокси. Часто используется в библиотеки

chi 27.06.2019 13:08

@chi, я запутался, но твой первый пример не компилируется. Второй неправильный: во-первых, вы немного изменили условие. Succ не является конструктором значений, как вы написали, но должен быть конструктором типа. И настоящая ошибка 2-го примера >:3:12: error: Not in scope: type constructor or class 'Succ'A data constructor of that name is in scope; did you mean DataKinds?

Vladimir 28.06.2019 06:56

Я имею в виду для 2-го образца: Main> type Foo = Succ Int<interactive>:3:12: error:Not in scope: type constructor or class 'Succ'A data constructor of that name is in scope; did you mean DataKinds?

Vladimir 28.06.2019 07:03

@Vladimir Вы не скопировали весь мой код на Haskell, включая строку, которая включает DataKinds

chi 28.06.2019 09:24

Я скопировал 1-й ваш образец и получил: C:_TMP\Haskell\tmp>ghci ans1.hs GHCi, версия 8.6.5: [1 из 1] Компиляция Main (ans1.hs, интерпретируется) ans1.hs:18: 37: ошибка: * Ожидается тип, но n' has kind N' * В сигнатуре типа выражения: n В первом аргументе toInt', namely (undefined :: n)' Во втором аргументе (+)', namely toInt (undefined :: n)' | 18 | toInt _ = 1 + toInt (не определено :: n) | ^ Ошибка, модули не загружены.

Vladimir 28.06.2019 11:40

Второй пример работает так, как вы описали, если я поместил type Foo = Succ Int в файл кода (а не в ghci, как раньше). Но роль Succ в вашем коде мне пока не ясна :(

Vladimir 28.06.2019 11:55

@ Владимир, я действительно забыл (undefined :: Proxy n) в своем первом фрагменте, извините. Это должно быть исправлено сейчас. О Succ: играет ту же роль, что и в вашем вопросе. Succ Int — это ошибка, так как Succ хочет число в N, а не тип — это то, что вы хотели, верно? Таким образом, вы можете использовать только Succ Zero, Succ (Succ Zero), ....

chi 28.06.2019 12:26

В моем коде (условиях, если хотите) Succ является конструктором типов, но в вашем коде Succ выглядит как конструктор данных (значений). Меня это смущает... Возможно, это форма DataKinds магии?

Vladimir 28.06.2019 12:52

@Vladimir Да, это DataKinds в действии. С этим расширением конструкторы уровня терминов, такие как Zero, Succ в моем коде, «поднимаются» на уровне типа. Таким образом, вы можете использовать их как обычно в значениях, но вы также можете использовать их в типах. Обозначение class Nat (n : N) указывает, что Nat — это не класс типов (мы не можем использовать Nat Bool), а класс N (мы можем использовать Nat Zero), поэтому он относится к поднятым значениям. Итак, у нас есть Int, Maybe Int, [Int] :: * как простые типы и Zero, Succ Zero, ... :: N как «числа на уровне типа». Здесь * и N называются «видами» (своего рода «тип типа»).

chi 28.06.2019 13:56

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