Обобщение суммы списков Haskell

Я работал на Python и создал пример того, как мы можем обобщить обычную функцию sumList для суммирования любого произвольного списка списков:

  1. Чтобы суммировать любой список чисел в Python, мы можем написать:
def sumList(xs): 
  if xs==[]:  
    return 0 
  else:  
    return xs[0]+sumList(xs[1:])

И в Хаскеле:

sumList [] = 0 
sumList (x:xs) = x + sumList xs
  1. но для суммирования произвольного вложенного списка в Python у нас есть:
def sumList2(xs): 
  if xs==[]:  
    return 0 
  elif isinstance(xs,int):  
    return xs 
  else:  
    return sumList2(xs[0])+sumList2(xs[1:])

Но следующий код Haskell выдает ошибку компилятора при выводе типа:

sumList [] = 0 
sumList (x:xs) = sumList x + sumList xs 
sumList x = x
<interactive>:2:38: error:
• Couldn't match expected type ‘t’ with actual type ‘[t]’
‘t’ is a rigid type variable bound by
the inferred type of sumList :: t -> [t]
at <interactive>:(1,1)-(3,13)
• In the first argument of ‘sumList’, namely ‘xs’
In the second argument of ‘(+)’, namely ‘sumList xs’
In the expression: sumList x + sumList xs
• Relevant bindings include
xs :: [t] (bound at <interactive>:2:12)
x :: t (bound at <interactive>:2:10)
sumList :: t -> [t] (bound at <interactive>:1:1)

Я использую IHaskell, насколько я понимаю, при использовании sumlist [] = 0 в качестве сопоставления с образцом Haskell делает вывод, что sumList :: [a] -> number, но затем, когда я передаю sumListx в следующей строке, x становится главой списка, поэтому sumList :: a -> number, поэтому получается абсурд . Я понял, что мне нужно сопоставить случай, когда входные данные представляют собой список, и выполнить две первые строки, а затем случай, когда входные данные являются целым числом, и выполнить последнюю строку, но я не могу понять, как это сделать.

Я пытаюсь создать тип суммы Either Int [a], но не знаю, будет ли это лучшим ответом, потому что я хочу, чтобы компилятор разобрался во всем, что связано с типами (в конечном итоге я напишу определение типа, но мой первоначальный тезис в том, что вопреки распространенному мнению, на Haskell можно проводить быстрые эксперименты, а это означает, что на первом этапе программирования не нужно беспокоиться о типах). Обратите внимание на недостатки цикла for: было бы очень сложно или невозможно обобщить сумму списка с использованием циклов for, поскольку из-за линейности цикла я думаю, что это очень убедительный аргумент в пользу масштабирования программного обеспечения для машинного обучения.

Стоит ли изучать 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
0
86
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Произвольно вложенный список представляет собой дерево.

data Tree a = Empty | Leaf a | Branch (Tree a) (Tree a)

Итак, ваша функция sumList2 в Python станет функцией sumTree в Haskell.

sumTree :: Tree Int -> Int
sumTree Empty = 0
sumTree (Leaf n) = n
sumTree (Branch l r) = sumTree l + sumTree r

Чтобы понять, почему произвольно вложенный список представляет собой дерево, давайте посмотрим на некоторые визуализации. Рассмотрим следующий список на Python.

primes = [2, 3, 5, 7]

В Haskell мы можем визуализировать этот список следующим образом.

 (:)
 / \
2  (:)
   / \
  3  (:)
     / \
    5  (:)
       / \
      7   []

Внутренние узлы — это конструкторы cons, т. е. (:). Левая сторона минусов — это голова, а правая — хвост. Левые дочерние узлы минусов являются значениями списка. Самый правый узел — это пустой список.

Как видите, список — это просто очень несбалансированное дерево.

Теперь рассмотрим следующий произвольно вложенный список в Python.

primes = [[2, [[], 3]], [5, [7, []]]]

В Haskell мы можем визуализировать этот список следующим образом.

     (:)
     / \
    /   \
   /     \
 (:)     (:)
 / \     / \
2  (:)  5  (:)
   / \     / \
 []   3   7   []

Как видите, оно похоже на дерево. Листья либо пусты, либо содержат какие-то данные. Списки в Haskell не могут представлять собой древовидную структуру. Следовательно, мы создаем тип данных Tree. Следовательно, давайте обновим визуализацию.

               Branch
                / \
               /   \
              /     \
             /       \
            /         \
           /           \
     Branch             Branch
      / \                / \
Leaf 2   Branch    Leaf 5   Branch
          / \                / \
     Empty   Leaf 3    Leaf 7   Empty

И именно поэтому произвольно вложенный список в Python становится деревом в Haskell.

Возможно, розовые деревья лучше соответствуют сути вопроса: data Tree a = Leaf a | Node [Tree a]

Naïm Favier 24.07.2024 18:03

@NaïmFavier Если мы используем розовые деревья, то регистр if xs==[]: и регистр else: в функции sumList2 OP будут объединены в один случай, и нам придется использовать map и foldr, чтобы преобразовать [Tree a] в Int.

Aadit M Shah 24.07.2024 18:09

Хорошо, достаточно справедливо.

Naïm Favier 24.07.2024 18:20

Я согласен с @NaïmFavier: бинарные деревья на самом деле не «работают» для этого. В качестве конкретного примера, каким деревьям, по вашему мнению, соответствуют [1, 2, 3], [[1, 2], 3] и [1, [2, 3]]? Кажется, что любое разумное картографирование будет объединять как минимум два из них.

Daniel Wagner 24.07.2024 19:06

большое спасибо!! идеальное объяснение

Agustin Bustos Barton 24.07.2024 19:06

@DanielWagner убедите себя, что существует функция f :: [Tree a] → Tree a такая, что f [1, 2, 3], f [f [1, 2], 3] и f [1, f [2, 3]] различны (используя fromInteger = Leaf).

Naïm Favier 24.07.2024 19:27

Явно, [1, 2, 3] будет отображаться в Branch (Leaf 1) $ Branch (Leaf 2) $ Branch (Leaf 3) $ Empty, а [1, [2, 3]] будет отображаться в Branch (Leaf 1) $ Branch (Branch (Leaf 2) $ Branch (Leaf 3) $ Empty) $ Empty.

Naïm Favier 24.07.2024 19:28

@NaïmFavier А что будет соответствовать Branch (Leaf 1) $ Branch (Leaf 2) $ Leaf 3?

Daniel Wagner 24.07.2024 20:18

Ах я вижу. Этого действительно нет в изображении предложенной мной карты. Так что розовые деревья подходят лучше: их можно сложить с помощью fold (Leaf a) = a; fold (Node xs) = concatMap fold xs.

Naïm Favier 24.07.2024 20:54

@DanielWagner Я основывал свой ответ на функции sumList2 OP, которая предлагает реализацию двоичного дерева. Да, [1, 2, 3] можно совместить либо с [[1, 2], 3], либо с [1, [2, 3]], но это нормально. Тип данных Tree, который я определил, — это свободный моноид. Empty — это Empty, Leaf — это Lift, а Branch — это Append. Следовательно, не имеет значения, какое представление мы выберем из-за ассоциативности. Мы могли бы даже смешивать и сочетать. Однако для ясности следует выбрать ассоциативность. Поскольку списки являются правоассоциативными, мы можем выбрать это в качестве ассоциативности по умолчанию при преобразовании.

Aadit M Shah 24.07.2024 20:54

Нет, бесплатный моноид на A — это [A]. Деревья не образуют моноид под Branch, потому что Branch не ассоциативен. Вам нужно будет факторизовать тип по отношению.

Naïm Favier 24.07.2024 20:56

Списки @NaïmFavier не являются бесплатными моноидами в Haskell. Источник: comonad.com/reader/2015/free-monoids-in-haskell.

Aadit M Shah 24.07.2024 20:58

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

Naïm Favier 24.07.2024 21:01

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

Naïm Favier 24.07.2024 21:02

@NaïmFavier Пусть ОП будет судить, полезен ли ему мой ответ или нет. Также, пожалуйста, перестаньте выдавать свое мнение за факты. Раздел комментариев моего ответа не место для этого.

Aadit M Shah 25.07.2024 06:44

Неправильный ответ вполне может оказаться полезным. Если комментарии не являются подходящим местом для указания на потенциальные проблемы с ответом, я не знаю, что это такое.

Naïm Favier 25.07.2024 10:59

@NaïmFavier Мой ответ неправильный. Вы можете использовать либо бинарные деревья, либо розовые деревья. Оба действительны. Прекратите эту бессмысленную дискуссию, или я привлечу модераторов.

Aadit M Shah 26.07.2024 07:30
Ответ принят как подходящий

Int, [Int], [[Int]] и т. д. — это разные типы, и тип функции выбирается в момент ее вызова. Если мы ожидаем, что наша функция вернет Int, и у нас есть два предложения

sumList (x:xs) = {- does not matter what goes here -}
sumList x = x

тогда мы гарантируем, что независимо от того, какой тип входных данных мы выберем, мы получим ошибку типа. Если мы выберем Int, то первое предложение будет ошибкой, поскольку x:xs может соответствовать только спискам; если мы выберем любой тип списка, то второе предложение будет ошибкой, поскольку оно не возвращает Int.

Однако не все потеряно. Единственное, что мы можем сделать, — это разделить эти два предложения на отдельные функции, которые имеют общее имя с использованием классов типов.

class Sums a where sums :: a -> Int
instance Sums Int where sums x = x
instance Sums a => Sums [a] where
    sums [] = 0
    sums (x:xs) = sums x + sums xs

Теперь, когда мы вызываем sums, мы никогда не вызываем функцию, имеющую оба предложения; вместо этого мы просим компилятор вызвать ту или иную из этих функций в зависимости от того, какой конкретный тип был выбран. Линия

    sums (x:xs) = sums x + sums xs

может выглядеть подозрительно, поскольку мы знаем, что x и xs имеют разные типы. Но нас спасает тот факт, что они вызывают не одну и ту же sums функцию! Опять же, тип функции выбирается в момент ее вызова, и здесь есть два разных вызова, которые могут выбирать два разных типа, после чего компилятор выберет разные функции для вызова.

Другой вариант, который у нас есть, — создать новый тип. Проблема с тем, что Int, [Int], [[Int]] и т. д. являются разными типами, заключается в том, что это означает, что мы должны статически знать, насколько глубоко вложен список. Если мы этого не знаем, то нам нужен тип, который может выразить тот факт, что его глубина вложенности известна только динамически, и который отслеживает данные, которые ему необходимо знать, чтобы определить, насколько глубока эта вложенность. Один из способов сделать это:

data DynList a = NotNested a | Nested [DynList a]

sumDynList :: DynList Int -> Int
sumDynList (NotNested x) = x
sumDynList (Nested []) = 0
sumDynList (Nested (x:xs)) = sumDynList x + sumDynList (Nested xs)

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

sumDynList (NotNested x) = x
sumDynList (Nested xs) = sum (map sumDynList xs)

Этот конкретный тип допускает различное количество вложений в разных точках структуры данных. Если вместо этого предполагается, что существует одна глубина вложенности и все элементы списка имеют одинаковую глубину, тогда нам нужен немного более продвинутый метод.

data SharedDepthList a = Depth0 a | Deeper (SharedDepthList [a])

sumSharedDepthList :: SharedDepthList Int -> Int
sumSharedDepthList = go id where
    go :: (a -> Int) -> SharedDepthList a -> Int
    go f (Depth0 x) = f x
    go f (Deeper xs) = go (sum . map f) xs

Я думаю, что в основном я не рекомендую это. Структуры данных с такой высокой точностью определения допустимых возможных значений на практике становятся довольно громоздкими.

спасибо большое!! предложенный тип данных динамического списка был тем, на что намекал мой мозг, но теперь я прекрасно понимаю. это небольшое обобщение типа данных первого комментария, но оба они очень полезны, с уважением!

Agustin Bustos Barton 24.07.2024 19:09

Обратите внимание, что встроенная библиотечная функция sum в Haskell полиморфна:

$ ghci
GHCi, version 9.4.5: https://www.haskell.org/ghc/  :? for help
ghci> 
ghci> :type  sum
sum :: (Foldable t, Num a) => t a -> a
ghci> 

Нам нужно найти трансформатор такого типа, который:

  1. может представлять собой «обобщенные списки»
  2. есть экземпляр Foldable.

К счастью, библиотека Haskell предоставляет такую ​​вещь под видом свободных монад.

По сути, у нас есть следующее определение типа:

data Free ft a = Pure a |  Impure (ft (Free ft a))

что, конечно, очень похоже на то, что дано в превосходном ответе Дэниела. Оказывается, если ft — функтор, то Free ft — монада, известная как «свободная» монада.

Например, правильное отображение [42,[5,10]] в таком контексте:

Impure [ Pure 42, Impure [ Pure 5, Pure 10 ] ]

Давайте попробуем использовать [] в качестве функтора:

ghci> 
ghci> import Control.Monad.Free
ghci> type GL = Free []
ghci> 
ghci> xss = Impure [ Pure 42, Impure [ Pure 5, Pure 10 ] ]  ::  GL Int
ghci> 
ghci> :instances GL
...
instance [safe] Foldable (Free [])
  -- Defined in ‘Control.Monad.Free’
...
ghci> 
ghci> :type xss
xss :: GL Int
ghci> 

Таким образом, библиотека предоставляет нам экземпляр Foldable, так что мы почти закончили.

ghci> 
ghci> foldl (+) 0 xss
57
ghci> sum xss
57
ghci> 

Вуаля!

вау, да, некоторые из моих интуиций о достоинствах Haskell для машинного обучения исходят из точки зрения теории категорий, по сути моя цель - создать 1 полиморфный алгоритм, а затем на его основе бесплатно попытаться обобщить, используя теоремы (суперэкспериментально для меня) , особенно с моим плохим знанием хаскеля и кота) большое спасибо!!

Agustin Bustos Barton 25.07.2024 00:40

Тем, кто следит за этим дома, может быть интересно соединить определение из этого ответа data Free ft a = Pure a | Impure (ft (Free ft a)) с определением из моего ответа data DynList a = NotNested a | Nested [DynList a]. Параллели не случайны!

Daniel Wagner 25.07.2024 18:57

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