Ограничение уровня типа Haskell

Мне любопытно, можно ли написать что-то подобное (псевдокод) в Haskell:

data Clock = Clock {hour :: (0 <= Int <= 24),  minutes :: (0 <= Int <= 60)}

сделать невозможным (на уровне типа) создание чего-то вроде этого:

let a = Clock 34 236

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


person user3680029    schedule 19.07.2021    source источник
comment
Вы можете работать с LiquidHaskell: ucsd-progsys.github.io/ Liquidhaskell-блог   -  person Willem Van Onsem    schedule 19.07.2021
comment
строго говоря, вы, конечно, можете определить два типа data Hour = HOne | HTwo | ... | HTwentyThree и data Minute = MOne | MTwo | ... | MFiftyNine, а затем определить часы как Clock { hour :: Hour, minutes :: Minute }.   -  person Willem Van Onsem    schedule 19.07.2021
comment
См. также Положительный целочисленный тип, в котором обсуждаются различные подходы, которые также были бы уместны здесь, и их компромиссы. Однако я думаю, что это не дубликат, поскольку решение @WillemVanOnsem там не указано.   -  person Daniel Wagner    schedule 19.07.2021
comment
Кстати, я думаю, что верхние границы не должны включать: < 24 и < 60.   -  person Noughtmare    schedule 19.07.2021
comment
Время 24:60, и вы слушаете, как ẓ̸̼̝̞̪͈̣́a̵̭͒l̵̨̛̰̪͎̈̃̈́̑̚͝g̵͇̲͓̠̜͈̈́̓̌̚͠o̴͖͇̥͑̌͌̈́͌ͅ ̷̡̛̳̳̟̱̳̦͘̕r̵̖̅͌̐a̸̡̝͑͝d̷͖͔̋̈͋̔͂̈́͝i̵̢̮̣͐͑͐̋͒ỗ̷̵̢̲̮̣͐͑͐̋͒ỗ̷̲   -  person Iceland_jack    schedule 21.07.2021


Ответы (2)


Конкретный пример лучше всего выражается с помощью типов из модуля . -арифметический пакет:

{-# LANGUAGE TypeOperators, TypeFamilies, DataKinds #-}

import Data.Modular

data Clock = Clock {hour :: Int/24,  minutes :: Int/60}

Обратите внимание, что верхние границы здесь исключающие, что, вероятно, должно было быть и в вашем коде.

Модульная арифметика — это частный случай частного типа, который в некоторых отношениях более практичен, чем ограниченные типы. Но у Haskell нет общей прямой поддержки ни для того, ни для другого. Как упомянул Виллем Ван Онсен, Liquid Haskell добавляет такую ​​функциональность. Не могу много сказать об этом, но я много раз слышал, что это работает на удивление хорошо.

person leftaroundabout    schedule 19.07.2021
comment
И, чтобы быть явным: / использует метод, предложенный в вопросе, а именно, используя своего рода построитель, который будет проверять и создавать допустимое модульное значение. (На жаргоне для этого используется умный конструктор.) - person Daniel Wagner; 19.07.2021
comment
Да, LH похож на волшебство. - person luqui; 20.07.2021

Smart constructors

Один простой способ сделать это — использовать интеллектуальную инкапсуляцию и интеллектуальные конструкторы:

module HoursAndMinutes (Hours, mkHours, getHours, Minutes, mkMinutes, getMinutes) where

newtype Hours = UnsafeHours Int

mkHours :: Int -> Maybe Hours
mkHours n | 0 <= n && n < 24 = Just (UnsafeHours n)
          | otherwise = Nothing

getHours :: Hours -> Int
getHours (UnsafeHours n) = n

newtype Minutes = UnsafeMinutes Int

mkMinutes :: Int -> Maybe Minutes
mkMinutes n | 0 <= n && n < 60 = Just (UnsafeMinutes n)
            | otherwise = Nothing

getMinutes :: Minutes -> Int
getMinutes (UnsafeMinutes n) = n

Теперь в вашем другом коде, если вы импортируете HoursAndMinutes, вы не можете построить недопустимые Hours или Minutes.

data Clock = Clock {hours :: Hours,  minutes :: Minutes}

Тогда это приведет к Nothing:

Clock <$> mkHours 34 <*> mkMinutes 236

Refined

Вы можете использовать встроенные предикаты из пакета refined, чтобы немного упростить этот подход:

data Clock = Clock
  { hours   :: Refined (FromTo 0 23) Int
  , minutes :: Refined (FromTo 0 59) Int
  }

Затем вы можете построить его с помощью:

Clock <$> refine 34 <*> refine 236

Который будет Left (RefineSomeException ...).

Недостатком по-прежнему является то, что проверки выполняются во время выполнения. Вы можете получить проверки времени компиляции с помощью Template Haskell:

Clock $$(refineTH 34) $$(refineTH 236)

Это вызовет исключение во время компиляции.

Liquid Haskell

Вы можете сделать это более удобным, используя Liquid Haskell, как предлагает Виллем ван Онсем в своем комментарии. :

module Clock where

data Clock = Clock { hours :: Int, minutes :: Int } deriving Show
{-@ data Clock = Clock
      { hours   :: {h:Int | 0 <= h && h < 24}
      , minutes :: {m:Int | 0 <= m && m < 60}
      } @-}

goodClock :: Clock
goodClock = Clock 16 49

badClock :: Clock
badClock = Clock 34 236 -- this gives an error

Попробуйте здесь.

person Noughtmare    schedule 19.07.2021
comment
Большое тебе спасибо. Liquid haskell выглядит очень чистым и интуитивно понятным. Я буду изучать эту библиотеку. Меня также соблазняет шаблон компоновщика comonad, но он выглядит довольно тяжелым решением. - person user3680029; 28.07.2021