Подтвердить что ты не робот

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

Итак, я пытаюсь научить себя Хаскелу. Я сейчас на 11-й главе Учите вас Haskell для Great Good, и я делаю 99 Haskell Problems, а также Project Euler Problems.

Все идет хорошо, но я постоянно делаю что-то, когда мне нужно отслеживать "переменные". Я просто создаю еще одну функцию, которая принимает эти "переменные" в качестве параметров и рекурсивно подает разные значения в зависимости от ситуации. Чтобы проиллюстрировать на примере, здесь мое решение Проблема 7 Project Euler, найдите 10001st prime:

answer :: Integer
answer = nthPrime 10001

nthPrime :: Integer -> Integer
nthPrime n
  | n < 1 = -1
  | otherwise = nthPrime' n 1 2 []


nthPrime' :: Integer -> Integer -> Integer -> [Integer] -> Integer
nthPrime' n currentIndex possiblePrime previousPrimes
  | isFactorOfAnyInThisList possiblePrime previousPrimes = nthPrime' n currentIndex theNextPossiblePrime previousPrimes
  | otherwise = 
    if currentIndex == n 
      then possiblePrime 
      else nthPrime' n currentIndexPlusOne theNextPossiblePrime previousPrimesPlusCurrentPrime
        where currentIndexPlusOne = currentIndex + 1
              theNextPossiblePrime = nextPossiblePrime possiblePrime
              previousPrimesPlusCurrentPrime = possiblePrime : previousPrimes

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

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

Ваша помощь очень ценится,

-Asaf

4b9b3361

Ответ 1

Вначале хорошо иметь императивное мышление. Со временем вы больше привыкнете к вещам и начнете видеть места, где вы можете иметь более функциональные программы. Практика делает совершенным.

Что касается работы с изменяемыми переменными, вы можете сохранить их до сих пор, если следовать основному правилу преобразования переменных в параметры функции и итерации в хвостовую рекурсию.

Ответ 2

Я застрял в менталитете императивного программирования и не обнимаю Хаскелл, как и я?

Ты не застрял, по крайней мере, я так не надеюсь. То, что вы переживаете, абсолютно нормально. Пока вы работали с императивными языками, вы узнали (возможно, не зная), чтобы увидеть проблемы программирования с очень определенной точки зрения, а именно с точки зрения машины Ван Неймана.

Если у вас есть проблема, например, составить список, содержащий некоторую последовательность чисел (скажем, мы хотим, чтобы первые 1000 четных чисел), вы сразу же думаете: реализация связанного списка (возможно, из стандартной библиотеки вашего язык программирования), цикл и переменную, которую вы задали начальному значению, а затем вы будете цитировать некоторое время, обновляя переменную, добавив 2 и поместив ее в конец списка.

Посмотрите, как вы в основном думаете обслуживать машину? Места памяти, петли и т.д.! В императивном программировании каждый думает о том, как манипулировать определенными ячейками памяти в определенном порядке, чтобы прийти к решению все время. (Это, кстати, одна из причин, по которой начинающие находят сложное программирование (императивное). Не программисты просто не используются для решения проблем, сводя их к последовательности операций с памятью. Почему они должны? Но как только вы узнали об этом, вы иметь власть - в императивном мире. Для функционального программирования вам нужно отучить это.)

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

constructStartingWith n = n : constructStartingWith (n+2)

И почти сделано! Чтобы прийти к нашему финальному списку, мы должны только сказать, с чего начать и сколько мы хотим:

result = take 1000 (constructStartingWith 0)

Обратите внимание, что в библиотеке имеется более общая версия constructStartingWith, она называется iterate, и она принимает не только начальное значение, но и функцию, которая делает следующий элемент списка из текущего:

iterate f n = n : iterate f (f n)
constructStartingWith = iterate (2+)   -- defined in terms of iterate

Другой подход состоит в том, чтобы предположить, что у нас был еще один список, из которого наш список можно было бы сделать легко. Например, если бы у нас был список первых n целых чисел, мы могли бы легко его перечислить в список четных целых чисел, умножив каждый элемент на 2. Теперь список первых 1000 (неотрицательных) целых чисел в Haskell просто

[0..999]

И есть функция map, которая преобразует списки, применяя данную функцию к каждому аргументу. Функция, которую мы хотим, состоит в том, чтобы удвоить элементы:

double n = 2*n

Следовательно:

result = map double [0..999]

Позже вы узнаете больше ярлыков. Например, нам не нужно определять double, но может использовать раздел: (2*) или мы могли бы написать наш список непосредственно в виде последовательности [0,2..1998]

Но не зная этих трюков еще не должно заставлять вас чувствовать себя плохо! Основная задача, с которой вы сейчас сталкиваетесь, - развить менталитет, когда вы видите, что проблема построения списка первых 1000 четных чисел является двухэтапной: а) определить, как выглядит список всех четных чисел, и b) принять определенную часть этого списка. После того, как вы начнете думать, что вы сделали , даже если вы все еще используете написанные вручную версии iterate и take.

Вернуться к проблеме Эйлера: здесь мы можем использовать метод сверху вниз (и несколько основных функций манипулирования списком, о которых действительно нужно знать: head, drop, filter, any). Во-первых, если бы у нас был список простых чисел, мы могли бы просто отказаться от первого 1000 и взять голову остальных, чтобы получить 1001-й:

result = head (drop 1000 primes)

Мы знаем, что после удаления любого числа элементов из бесконечного списка все равно останется непустой список, чтобы выбрать head из, следовательно, использование head здесь оправдано. Когда вы не уверены, есть ли более 1000 простых чисел, вы должны написать что-то вроде:

result = case drop 1000 primes of
    [] -> error "The ancient greeks were wrong! There are less than 1001 primes!"
    (r:_) -> r

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

primes = 2 : {-an infinite list of numbers that are prime-}

Мы точно знаем, что 2 - это первое простое, базовый случай, так сказать, поэтому мы можем записать его. Незаполненная часть дает нам о чем подумать. Например, список должен начинаться с некоторого значения, которое по очевидной причине больше 2. Следовательно, уточнено:

primes = 2 : {- something like [3..] but only the ones that are prime -}

Теперь это тот момент, когда возникает шаблон, который нужно научиться распознавать. Это, безусловно, список filter ed предикатом, а именно простота (не имеет значения, что мы еще не знаем, как проверить правильность, логическая структура является важной точкой. (И мы можем быть что тест на простоту возможен!)). Это позволяет нам написать больше кода:

primes = 2 : filter isPrime [3..]

См? Мы почти закончили. В 3 этапа мы уменьшили довольно сложную задачу таким образом, чтобы все, что осталось писать, было довольно простым предикатом. Опять же, мы можем писать в псевдокоде:

isPrime n = {- false if any number in 2..n-1 divides n, otherwise true -}

и может уточнить это. Поскольку это уже почти haskell, это слишком просто:

isPrime n = not (any (divides n) [2..n-1])
divides n p = n `rem` p == 0

Обратите внимание, что мы еще не сделали оптимизацию. Например, мы можем сконструировать список, который нужно отфильтровать, чтобы содержать только нечетные числа, так как мы знаем, что даже единицы не являются первичными. Что еще более важно, мы хотим уменьшить количество кандидатов, которые мы должны попробовать в isPrime. И здесь необходимы некоторые математические знания (то же самое было бы верно, если бы вы запрограммировали это на С++ или Java, конечно), что говорит нам, что достаточно проверить, что тестируемый n делится на любое простое число, и что нам не нужно проверять делимость на простые числа, квадрат которых больше, чем n. К счастью, мы уже определили список простых чисел и можем выбрать набор кандидатов оттуда! Я оставляю это как упражнение.

Вы узнаете позже, как использовать стандартную библиотеку и синтаксические сахара, такие как разделы, списки и т.д., и вы постепенно откажетесь от своих собственных основных функций.

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

Удачи!

Ответ 3

Сверху моей головы:

Ответ 4

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

nthPrime n = primes !! (n - 1) where
  primes = filter isPrime [2..]
  isPrime p = isPrime' p [2..p - 1]
  isPrime' p [] = True
  isPrime' p (x:xs) 
    | (p `mod` x == 0) = False
    | otherwise = isPrime' p xs

Eg nthPrime 4 возвращает 7. Несколько замечаний:

  • Функция isPrime' использует сопоставление шаблонов для реализации функции, а не полагается на операторы if.
  • Значение primes представляет собой бесконечный список всех простых чисел. Поскольку haskell ленив, это вполне приемлемо.
  • filter используется, а не переопределяет это поведение с помощью рекурсии.

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

Ответ 5

Другой подход, только для разнообразия! Сильное использование лени...

module Main where

nonmults :: Int -> Int -> [Int] -> [Int]
nonmults n next [] = []
nonmults n next [email protected](x:xs)
   | x < next = x : nonmults n next xs
   | x == next = nonmults n (next + n) xs
   | otherwise = nonmults n (next + n) l

select_primes :: [Int] -> [Int]
select_primes [] = []
select_primes (x:xs) = 
  x : (select_primes $ nonmults x (x + x) xs)

main :: IO ()
main = do
  let primes = select_primes [2 ..]
  putStrLn $ show $ primes !! 10000 -- the first prime is index 0 ...

Ответ 6

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

1. Хаскелл прост

Haskell и другие функциональные языки, с которыми я не знаком, безусловно, сильно отличаются от ваших "нормальных" языков, таких как C, Java, Python и т.д. К сожалению, как работает наша психика, люди преждевременно делают вывод, что если что-то другое, тогда A) они этого не понимают, а B) это сложнее, чем то, что они уже знают. Если мы посмотрим на Хаскелла очень объективно, мы увидим, что эти две гипотезы полностью ложны:

"Но я этого не понимаю:("

На самом деле вы это делаете. Все в Haskell и других функциональных языках определяется с точки зрения логики и шаблонов. Если вы можете ответить на вопрос так же просто, как "Если все Меины - это плывет, а все плывущие маны, все ли Меиды мавров?", Тогда вы, вероятно, могли бы сами написать Прелюдию Хаскелла. Чтобы далее поддержать этот момент, считайте, что Списки Haskell определены в терминах Haskell и не являются особой магией вуду.

"Но это осложняет"

На самом деле все наоборот. Простота настолько голая и голая, что наши мозги не могут понять, что с ней делать вначале. По сравнению с другими языками у Haskell на самом деле значительно меньше "функций" и гораздо меньше синтаксиса. Когда вы читаете код Haskell, вы заметите, что почти все определения функций выглядят одинаково стилистически. Это совсем не так, как например Java, который имеет такие конструкции, как Classes, Interfaces, для циклов, блоков try/catch, анонимных функций и т.д.... каждый со своим синтаксисом и идиомами.

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

2. Нет никакой версии Haskell

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

Многие новички задают такие вопросы, как "Как сделать цикл for в Haskell?" и невинные люди, которые просто пытаются помочь, приведут к неудачному ответу, вероятно, включив вспомогательную функцию и дополнительный параметр Int, а хвост рекурсирует, пока вы не достигнете 0. Конечно, эта конструкция может вычислить что-то вроде цикла for, но без путь - это цикл for, это не замена цикла for, и ни в коем случае это даже не похоже на цикл for, если вы считаете поток исполнения. Аналогичная государственная монада для моделирования состояния. Его можно использовать для выполнения аналогичных действий, как статические переменные на других языках, но ни в коем случае это одно и то же. Большинство людей оставляют за собой последний лакомый кусочек об этом, не будучи тем же самым, когда они отвечают на эти вопросы, и я думаю, что это только смущает людей больше, пока они не осознают это самостоятельно.

3. Haskell - логический движок, а не язык программирования

Это, вероятно, наименее верная точка, которую я пытаюсь сделать, но выслушайте меня. На императивных языках программирования мы заботимся о том, чтобы заставить наши машины делать что-то, выполнять действия, изменять состояние и т.д. В Haskell мы пытаемся определить, что это такое и как они должны себя вести. Обычно нас не волнует то, что что-то делает в любой момент. Это, безусловно, имеет преимущества и недостатки, но это так. Это очень отличается от того, что большинство людей думает, когда вы говорите "язык программирования".

Итак, чтобы понять, как оставить императивный образ мышления и перейти к более функциональному мышлению. Понимая, насколько разумный Haskell поможет вам больше не смотреть на свой собственный код. Надеемся, что мышление об этом Haskell поможет вам стать более продуктивным Haskeller.