Я работал на Python и создал пример того, как мы можем обобщить обычную функцию sumList
для суммирования любого произвольного списка списков:
def sumList(xs):
if xs==[]:
return 0
else:
return xs[0]+sumList(xs[1:])
И в Хаскеле:
sumList [] = 0
sumList (x:xs) = x + sumList xs
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
, но затем, когда я передаю sumList
x
в следующей строке, x
становится главой списка, поэтому sumList :: a -> number
, поэтому получается абсурд . Я понял, что мне нужно сопоставить случай, когда входные данные представляют собой список, и выполнить две первые строки, а затем случай, когда входные данные являются целым числом, и выполнить последнюю строку, но я не могу понять, как это сделать.
Я пытаюсь создать тип суммы Either Int [a]
, но не знаю, будет ли это лучшим ответом, потому что я хочу, чтобы компилятор разобрался во всем, что связано с типами (в конечном итоге я напишу определение типа, но мой первоначальный тезис в том, что вопреки распространенному мнению, на Haskell можно проводить быстрые эксперименты, а это означает, что на первом этапе программирования не нужно беспокоиться о типах). Обратите внимание на недостатки цикла for: было бы очень сложно или невозможно обобщить сумму списка с использованием циклов for, поскольку из-за линейности цикла я думаю, что это очень убедительный аргумент в пользу масштабирования программного обеспечения для машинного обучения.
Произвольно вложенный список представляет собой дерево.
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.
@NaïmFavier Если мы используем розовые деревья, то регистр if xs==[]:
и регистр else:
в функции sumList2
OP будут объединены в один случай, и нам придется использовать map
и foldr
, чтобы преобразовать [Tree a]
в Int
.
Хорошо, достаточно справедливо.
Я согласен с @NaïmFavier: бинарные деревья на самом деле не «работают» для этого. В качестве конкретного примера, каким деревьям, по вашему мнению, соответствуют [1, 2, 3]
, [[1, 2], 3]
и [1, [2, 3]]
? Кажется, что любое разумное картографирование будет объединять как минимум два из них.
большое спасибо!! идеальное объяснение
@DanielWagner убедите себя, что существует функция f :: [Tree a] → Tree a
такая, что f [1, 2, 3]
, f [f [1, 2], 3]
и f [1, f [2, 3]]
различны (используя fromInteger = Leaf
).
Явно, [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ïmFavier А что будет соответствовать Branch (Leaf 1) $ Branch (Leaf 2) $ Leaf 3
?
Ах я вижу. Этого действительно нет в изображении предложенной мной карты. Так что розовые деревья подходят лучше: их можно сложить с помощью fold (Leaf a) = a; fold (Node xs) = concatMap fold xs
.
@DanielWagner Я основывал свой ответ на функции sumList2
OP, которая предлагает реализацию двоичного дерева. Да, [1, 2, 3]
можно совместить либо с [[1, 2], 3]
, либо с [1, [2, 3]]
, но это нормально. Тип данных Tree
, который я определил, — это свободный моноид. Empty
— это Empty
, Leaf
— это Lift
, а Branch
— это Append
. Следовательно, не имеет значения, какое представление мы выберем из-за ассоциативности. Мы могли бы даже смешивать и сочетать. Однако для ясности следует выбрать ассоциативность. Поскольку списки являются правоассоциативными, мы можем выбрать это в качестве ассоциативности по умолчанию при преобразовании.
Нет, бесплатный моноид на A
— это [A]
. Деревья не образуют моноид под Branch
, потому что Branch
не ассоциативен. Вам нужно будет факторизовать тип по отношению.
Списки @NaïmFavier не являются бесплатными моноидами в Haskell. Источник: comonad.com/reader/2015/free-monoids-in-haskell.
Я знаю, но они ближе к этому названию, чем бинарные деревья без частностей. Если хотите, я работаю над идеализированным подмножеством Haskell, состоящим из конечных, полных программ.
В любом случае, интенсиональное определение функции OP не имеет отношения к ее типу, и именно это должно определять дизайн наших деревьев. Его входной тип A
таков, что A
является либо целым числом, либо списком A
; преобразование этого в тип данных дает розовые деревья, а не двоичные деревья.
@NaïmFavier Пусть ОП будет судить, полезен ли ему мой ответ или нет. Также, пожалуйста, перестаньте выдавать свое мнение за факты. Раздел комментариев моего ответа не место для этого.
Неправильный ответ вполне может оказаться полезным. Если комментарии не являются подходящим местом для указания на потенциальные проблемы с ответом, я не знаю, что это такое.
@NaïmFavier Мой ответ неправильный. Вы можете использовать либо бинарные деревья, либо розовые деревья. Оба действительны. Прекратите эту бессмысленную дискуссию, или я привлечу модераторов.
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
Я думаю, что в основном я не рекомендую это. Структуры данных с такой высокой точностью определения допустимых возможных значений на практике становятся довольно громоздкими.
спасибо большое!! предложенный тип данных динамического списка был тем, на что намекал мой мозг, но теперь я прекрасно понимаю. это небольшое обобщение типа данных первого комментария, но оба они очень полезны, с уважением!
Обратите внимание, что встроенная библиотечная функция 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>
Нам нужно найти трансформатор такого типа, который:
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 полиморфный алгоритм, а затем на его основе бесплатно попытаться обобщить, используя теоремы (суперэкспериментально для меня) , особенно с моим плохим знанием хаскеля и кота) большое спасибо!!
Тем, кто следит за этим дома, может быть интересно соединить определение из этого ответа data Free ft a = Pure a | Impure (ft (Free ft a))
с определением из моего ответа data DynList a = NotNested a | Nested [DynList a]
. Параллели не случайны!
Возможно, розовые деревья лучше соответствуют сути вопроса:
data Tree a = Leaf a | Node [Tree a]