Если я пришел из опыта программирования, как мне обдумать идею отсутствия динамических переменных для отслеживания вещей в Haskell? - PullRequest
13 голосов
/ 09 декабря 2011

Итак, я пытаюсь научить себя Хаскеллу.В настоящее время я нахожусь на 11-й главе Learn You a Haskell for Great Good и выполняю 99 Задачи Haskell , а также Project Euler Задачи

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

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, как должен?И если да, то, как я себя чувствую, как этого избежать?Есть ли книга или источник, на который вы можете указать мне, что может помочь мне думать более как на Хаскеле?

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

-Asaf

Ответы [ 6 ]

14 голосов
/ 09 декабря 2011

Я застрял в императивном мышлении программирования и не понимаю Haskell, как я должен?

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

Если у вас есть проблема, скажем, составления списка, который содержит некоторую последовательность чисел (допустим, мы хотим, чтобы первые 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 четных чисел состоит из двух этапов: выглядит и б) взять определенную часть этого списка. Как только вы начинаете думать таким образом, вы делаете , даже если вы все еще используете рукописные версии 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, отредактированный предикатом , а именно, простотой (не имеет значения, что мы еще не знаем, как проверить простоту, важна логическая структура. (И мы можем быть уверены, что тест на первичность возможен!)). Это позволяет нам писать больше кода:

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. И здесь требуются некоторые математические знания (то же самое будет верно, если вы запрограммируете это на C ++ или Java, конечно), что говорит нам, что достаточно проверить, является ли n, который мы проверяем, делимым на любое простое число и что нам не нужно проверять делимость на простые числа, площадь которых больше n. К счастью, мы уже определили список простых чисел и можем выбрать набор кандидатов оттуда! Я оставляю это как упражнение.

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

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

Веселись!

12 голосов
/ 09 декабря 2011

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

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

4 голосов
/ 09 декабря 2011

С макушки головы:

3 голосов
/ 09 декабря 2011

Я думаю, что большое изменение от вашего кода к более похожему на 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

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

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

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

2 голосов
/ 10 декабря 2011

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

module Main where

nonmults :: Int -> Int -> [Int] -> [Int]
nonmults n next [] = []
nonmults n next l@(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 ...
1 голос
/ 16 декабря 2013

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

1.Haskell - это simple

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

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

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

«Но это сложно»

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

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

2.Не существует версии Haskell чего-либо

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...