Лучше ли использовать guards, чем шаблоны для рекурсивных функций в Haskell?

мне просто интересно о рекурсивной функции, которую я выкладываю в Haskell. Обычно ли лучше использовать guards, чем шаблоны для рекурсивных функций?

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

units :: Int -> String

units 0 = "zero"
units 1 = "one"

гораздо предпочтительнее

units n
    | n == 0 = "zero"
    | n == 1 = "one"

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

просто не совсем уверен в терминологии: я использую что-то вроде этого:

f y [] = [] 
f y (x:xs) 
    | y == 0 = ...... 
    | otherwise = ...... 

или это было бы лучше?

f y [] = [] 
f 0 (x:xs) = 
f y (x:xs) =

5 ответов


мое общее правило будет таким:

  • используйте сопоставление шаблонов, когда охранник будет простым == проверка.

с рекурсией вы обычно проверяете базовый случай. Поэтому, если ваш базовый случай простой == проверьте, затем используйте сопоставление шаблонов.

поэтому я обычно делаю это:

map f [] = []
map f (x:xs) = f x : map f xs

вместо этого (null просто проверяет, пуст ли список. Это в основном == []):

map f xs | null xs   = []
         | otherwise = f (head xs) : map f (tail xs)

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

[обновление]

для вашего конкретного случая, я бы сделала что-то вроде этого:

f _ []      = []
f 0 _       = ...
f y (x:xs)  = ...

шаблон соответствует, как охранники, падают сверху вниз, останавливаясь на первом определении, которое соответствует входу. Я использовал символ подчеркивания, чтобы указать что для первого матча шаблона, мне было все равно, что y аргумент был, и для второго соответствия шаблону мне было все равно, что такое аргумент списка (хотя, если вы используете список в этом вычислении, то вы не должны использовать подчеркивание). Поскольку это все еще довольно просто ==-как проверки, я лично придерживаюсь сопоставления шаблонов.

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


простое правило

  • если вы не рекурсией по структуре данных, использовать поиск по шаблону
  • если рекурсивное состояние более сложное, используйте guards.

Обсуждение

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

для вашего примера сопоставление шаблонов на целых числах, очевидно, чище и эффективнее:

units 0 = "zero"
units 1 = "one"

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

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


в этом нет очень жестких и быстрых правил, поэтому ответы, которые вы получили, были немного туманными. Некоторые решения просты, например, сопоставление шаблонов на [] вместо охраны с f xs | length xs == 0 = ... что ужасно во многих отношениях. Но когда нет убедительной практической проблемы, просто используйте то, что делает код более ясным.

в качестве примера, рассмотрим эти функции (которые на самом деле не делает ничего полезного, просто служить иллюстрации):

f1 _ [] = [] 
f1 0 (x:xs) = [[x], xs]
f1 y (x:xs) = [x] : f1 (y - 1) xs

f2 _ [] = []
f2 y (x:xs) | y == 0    = calc 1 : f2 (- x) xs
            | otherwise = calc (1 / y) : f2 (y * x) xs
  where calc z = x * ...

на f1, отдельные шаблоны подчеркивают, что рекурсия имеет два базовых случая. В f2, охранники подчеркивают, что 0 - это просто частный случай для некоторых вычислений (большинство из которых выполняются calc, определена в where предложение, разделяемое обеими ветвями guard) и не изменяет структуру вычисления.


@Dan правильно: это в основном вопрос личных предпочтений и не влияет на сгенерированный код. Этот модуль:

module Test where

units :: Int -> String
units 0 = "zero"
units 1 = "one"

unitGuarded :: Int -> String
unitGuarded n
  | n == 0 = "zero"
  | n == 1 = "one"

произвел следующее ядро:

Test.units =
  \ (ds_dkU :: GHC.Types.Int) ->
    case ds_dkU of _ { GHC.Types.I# ds1_dkV ->
    case ds1_dkV of _ {
      __DEFAULT -> Test.units3;
      0 -> Test.unitGuarded2;
      1 -> Test.unitGuarded1
    }
    }

Test.unitGuarded =
  \ (n_abw :: GHC.Types.Int) ->
    case n_abw of _ { GHC.Types.I# x_ald ->
    case x_ald of _ {
      __DEFAULT -> Test.unitGuarded3;
      0 -> Test.unitGuarded2;
      1 -> Test.unitGuarded1
    }
    }

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


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

при выполнении сопоставления шаблонов вы можете безопасно получить доступ к внутренней структуре объекта, не опасаясь, что этот объект является чем-то другим. Если вы забыли некоторые шаблоны, компилятор может предупредить вас (к сожалению, это предупреждение отключено по умолчанию в GHC).

например, при написании это:

map f xs | null xs   = []
         | otherwise = f (head xs) : map f (tail xs)

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

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

примеры:

-- compiles, crashes in runtime
map f xs | not (null xs)   = []
         | otherwise = f (head xs) : map f (tail xs)

-- does not have any way to compile
map f (h:t) = []
map f [] = f h : map f t


-- does not give any warnings
map f xs = f (head xs) : map f (tail xs)

-- can give a warning of non-exhaustive pattern match
map f (h:t) = f h : map f t