Класс вариативных функций в Haskell

Можно ли определить класс типов в Haskell таким образом, чтобы функция могла выполнять определенное поведение независимо от количества параметров? Например:

{-# LANGUAGE FlexibleInstances #-}

class FillZeroes ... where
  ...

f :: Num a => a -> a -> a
f x y = x+y+1

g :: Num a => a -> a
g x = 10*x

h :: (Eq a,Num a) => a -> Bool
h 0 = False
h _ = True

fillZeroes f === f 0 0 === 1
fillZeroes g === g 0 === 0
fillZeroes h === h 0 === False

Должно быть возможно, да. Вы изучали реализацию других функций с переменным числом аргументов в Haskell? Думали ли вы, что должен предоставлять ваш класс типов FilleZeroes? Вы пытались реализовать экземпляр для его функций?

Bergi 26.12.2020 16:49

«Возможно ли определить класс типов в Haskell таким образом, чтобы функция могла выполнять определенное поведение независимо от количества параметров?» Да, см., например. Класс Testable типа QuickCheck для примера.

Mark Seemann 26.12.2020 16:53

Хотя это возможно, я бы не рекомендовал делать это с вариативным обманом. Я думаю, что вы действительно должны использовать, например. f :: (Double,Double) -> Double, g :: Double -> Double, h :: Int -> Bool, и тогда вы можете оценить их все как f zeroV/g zeroV/h zeroV с zeroV::AdditiveGroup v => v.

leftaroundabout 26.12.2020 17:16
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
168
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Функции с переменным числом, конечно, возможны, но «заполнение» нулями немного сложно. Основная причина этого в том, что GHC на самом деле не знает, сколько нужно заполнить! Возьми свою функцию f. Вам может показаться очевидным, что «заполнение нулями» означает применение 0 к f дважды, но вы уверены? Что касается GHC, возможно, вы хотите, чтобы ваш результат был функцией из a -> a. Или, может быть, вы создали у функций типа Int -> Int экземпляр Num — тогда, возможно, fillZeros следует сначала предоставить два аргумента 0 :: Int -> Int для f, а затем еще и 0 :: Int. Есть так много вариантов!

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


Хорошая начальная попытка для класса:

{-# LANGUAGE MultiParamTypeClasses #-}

class FillZeroes x y where
  fillZeroes :: x -> y

instance (Num a, FillZeroes b c) => FillZeroes (a -> b) c where
  fillZeroes f = fillZeroes (f 0)

Здесь fillZeroes — это функция, которая принимает x и производит y, и мы создали экземпляр, где x — это тип функции a -> b (где Num a). Обратите внимание, как это выглядит рекурсивно, с ограничением FillZeroes b c и вызовом fillZeroes (f 0) в реализации. Если это рекурсивный случай, то, очевидно, нам понадобится базовый случай. Как мы можем написать это? Самый простой вариант — использовать перекрывающийся экземпляр:

instance {-# OVERLAPPABLE #-} FillZeroes a a where
  fillZeroes = id

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

{-# LANGUAGE TypeApplications #-}

> fillZeroes (f @Int) :: Int
1
> fillZeroes (f @Int 4) :: Int
5
> fillZeroes (g @Int) :: Int
0
> fillZeroes (h @Int) :: Bool
False

Можем ли мы сделать лучше? Почему нам нужно сообщать GHC как конкретный тип функции (что мы делаем с помощью приложения типа @Int), так и тип результата? Проблема в том, что GHC необходимо точно знать оба типа x и y, чтобы получить правильные экземпляры классов типов. Как оказалось, мы можем использовать семейства типов, чтобы обойти это препятствие. Рассмотрим следующее семейство типов:

{-# LANGUAGE TypeFamilies #-}

type family FZResult a where
  FZResult (a -> b) = FZResult b
  FZResult a = a

Это (закрытое) семейство типов дает тип результата, который мы ищем. Итак, мы можем добавить это в наш класс вместо параметра y:

class FillZeroes a where
  fillZeroes :: a -> FZResult a

instance {-# OVERLAPPABLE #-} FZResult a ~ a => FillZeroes a where
  fillZeroes = id

instance (Num b, FillZeroes a) => FillZeroes (b -> a) where
  fillZeroes f = fillZeroes (f 0)

Теперь нам нужно только сообщить GHC, каков конкретный тип аргумента, и он может использовать семейство типов, чтобы определить, сколько нулей нужно заполнить:

> fillZeroes (f @Int)
1
> fillZeroes (f @Int 4)
5
> fillZeroes (g @Int)
0
> fillZeroes (h @Int)
False

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