Границы типов чисел в Common LISP и стеке перетекают в GHCI

Первый вопрос здесь, новичок в Common LISP и Haskell, пожалуйста, будьте добры. У меня есть функция в Common LISP — код ниже — которая предназначена для определения того, является ли площадь треугольника целым числом (целым?).

(defun area-int-p (a b c)
  (let* ((s (/ (+ a b c) 2))
         (area (sqrt (* s (- s a) (- s b) (- s c)))))
    (if (equal (ceiling area) (floor area))
        t
        nil)))

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

CL-USER> (area-int-p 333 333 333)
NIL

Идеально! Оно работает. Чтобы проверить это еще больше, я запустил его с аргументами 3333. Вот что я получил взамен:

CL-USER> (area-int-p 3333 3333 3333)
T

Что-то не так, такого быть не должно! Итак, я пробую следующую, надеюсь, эквивалентную функцию Haskell, чтобы посмотреть, что произойдет:

areaIntP :: (Integral a) => a -> a -> a -> Bool
areaIntP a b c =
  let aa = fromIntegral a
      bb = fromIntegral b
      cc = fromIntegral c
      perimeter = aa + bb + cc
      s = perimeter/2
      area = sqrt(s * (s - aa) * (s - bb) * (s - cc))
  in  if ceiling area == floor area
      then True
      else False

Вот что я получаю:

*Main> areaIntP 3333 3333 3333
False
*Main> areaIntP 333 333 333
False

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

toplamArtilar :: Integral a => a -> a -> a -> a
toplamArtilar altSinir ustSinir toplam =
  if ustSinir == altSinir
  then toplam
  else if areaIntP ustSinir ustSinir (ustSinir + 1) == True
       then toplamArtilar altSinir (ustSinir - 1) (toplam + (3 * ustSinir + 1))
       else toplamArtilar altSinir (ustSinir - 1) toplam

toplamEksiler :: Integral a => a -> a -> a -> a
toplamEksiler altSinir ustSinir toplam =
  if ustSinir == altSinir
  then toplam
  else if areaIntP ustSinir ustSinir (ustSinir - 1) == True
       then toplamEksiler altSinir (ustSinir - 1) (toplam + (3 * ustSinir - 1))
       else toplamEksiler altSinir (ustSinir - 1) toplam

sonuc altSinir ustSinir =
  toplamEksiler altSinir ustSinir (toplamArtilar altSinir ustSinir 0)

(ustSinir означает верхний предел, altSinir нижний предел, кстати.) Однако запуск sonuc с аргументами 2 и 333333333 приводит к переполнению моего стека. При выполнении эквивалентных функций в Common LISP со стеком все в порядке, но функция area-int-p ненадежна, вероятно, из-за ограничений числового типа, которые интерпретатор выводит. После всего этого мой вопрос двоякий:

1) Как обойти проблему в функции Common LISP area-int-p?

2) Как предотвратить переполнение стека с помощью функций Haskell, описанных выше, либо в Emacs, либо при запуске GHCi из терминала?

Примечание для тех, кто понимает, чего я пытаюсь добиться: пожалуйста, не говорите мне использовать Java BigDecimal и BigInteger.

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


person zweiblumen    schedule 06.10.2016    source источник
comment
Это должны быть два отдельных вопроса.   -  person Reid Barton    schedule 07.10.2016
comment
Ты прав. Я спросил их вместе, потому что подумал, что, может быть, обе проблемы как-то связаны с ограничением памяти в emacs и могут быть исправлены путем изменения какого-то параметра, связанного с этим. Конечно, это было до того, как я увидел ответ Луки о части Haskell.   -  person zweiblumen    schedule 07.10.2016


Ответы (3)


Давайте определим промежуточную функцию Common Lisp:

(defun area (a b c)
  (let ((s (/ (+ a b c) 2)))
    (sqrt (* s (- s a) (- s b) (- s c)))))

Ваши тесты дают:

CL-USER> (area 333 333 333)
48016.344

CL-USER> (area 3333 3333 3333)
4810290.0

Во втором случае должно быть понятно, что и потолок, и пол равны. Это не относится к Haskell, где второй тест с 3333 возвращает:

4810290.040910754

Плавающая точка

В Common Lisp значение, из которого мы извлекаем квадратный корень:

370222244442963/16 

Это потому, что вычисления производятся с рациональными числами. До этого момента точность максимальна. Однако SQRT может возвращать либо рациональный, если возможно, либо приблизительный результат. Как особый случай, результат может быть целым числом в некоторых реализациях, как указал Райнер Джосвиг в комментарии. Это имеет смысл, потому что и integer, и ratio являются непересекающимися подтипами рационального типа. Но, как показывает ваша проблема, некоторые квадратные корни иррациональны (например, √2), и в этом случае CL ​​может возвращать число с плавающей запятой, приближающееся к значению (или сложное число с плавающей запятой).

Соответствующий раздел, касающийся чисел с плавающей запятой и математических функций, приведен в разделе 12.1.3.3 Правило заменяемости чисел с плавающей запятой. Короче говоря, результат преобразуется в single-float, когда вы вычисляете квадратный корень, что приводит к некоторой потере точности. Чтобы иметь двойника, вы должны быть более явными:

(defun area (a b c)
   (let ((s (/ (+ a b c) 2)))
     (sqrt (float (* s (- s a) (- s b) (- s c)) 0d0))))

Я также мог бы использовать (coerce ... 'double-float), но здесь я решил вызвать функцию преобразования FLOAT. Необязательный второй аргумент представляет собой прототип с плавающей запятой, т.е. значение целевого типа. Выше это 0d0, двойной поплавок. Вы также можете использовать 0l0 для длинных двойников или 0s0 для коротких. Этот параметр полезен, если вы хотите иметь ту же точность, что и входное число с плавающей запятой, но его можно использовать и с литералами, как в примере. Точное значение типов short, single, double или long float определяется реализацией, но они должны учитывать некоторые правила. Текущие реализации обычно дают большую точность, чем требуемый минимум.

CL-USER> (area 3333 3333 3333)
4810290.040910754d0

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

CL-USER> (zerop (nth-value 1 (truncate 4810290.040910754d0)))
NIL

Произвольной точности

Обратите внимание, что независимо от языка реализации (Haskell, CL или другой) подход будет давать неверные результаты для некоторых входных данных, учитывая то, как представлены числа с плавающей запятой. Действительно, та же проблема, что и с CL, может возникнуть для некоторых входных данных с более точными числами с плавающей запятой, где результат будет очень близок к целому числу. Вам может понадобиться другой математический подход или что-то вроде MPFR для вычислений с плавающей запятой произвольной точности. SBCL поставляется с sb-mpfr:

CL-USER> (require :sb-mpfr)
("SB-MPFR" "SB-GMP")

CL-USER> (in-package :sb-mpfr)
#<PACKAGE "SB-MPFR">

А потом:

SB-MPFR> (with-precision 256
           (sqrt (coerce 370222244442963/16 'mpfr-float)))
.4810290040910754427104204965311207243133723228299086361205561385039201180068712e+7
-1
person coredump    schedule 07.10.2016
comment
Да, обратите внимание, что в некоторых реализациях результатом (sqrt 9.0) может быть целое число. - person Rainer Joswig; 07.10.2016
comment
например, (- (floor 370222244442964) (floor 370222244442963)) это 1, а (- (floor 370222244442964.0) (floor 370222244442963.0)) это 0. - person Will Ness; 07.10.2016

Я отвечу на ваш второй вопрос, я не уверен насчет первого. В Haskell, поскольку это ленивый язык, когда вы используете хвостовую рекурсию с параметром-аккумулятором, может иметь место «накопление переходников». Преобразователь — это выражение, которое приостановлено и еще не вычислено. Возьмем более простой пример, суммируя все числа от 0 до n:

tri :: Int -> Int -> Int
tri 0 accum = accum
tri n accum = tri (n-1) (accum + n)

Если мы проследим оценку, мы увидим, что происходит:

tri 3 0
  = tri (3-1) (0+3)
  = tri 2 (0+3)
  = tri (2-1) ((0+3)+2)
  = tri 1 ((0+3)+2)
  = tri (1-1) (((0+3)+2)+1)
  = tri 0 (((0+3)+2)+1)
  = ((0+3)+2)+1     -- here is where ghc uses the C stack
  = (0+3)+2        (+1) on stack
  = 0+3            (+2) (+1) on stack
  = 0              (+3) (+2) (+1) on stack
  = 3              (+2) (+1) on stack
  = 5              (+1) on stack
  = 6             

Это, конечно, упрощение, но это интуиция, которая может помочь вам понять как переполнение стека, так и утечку пространства, вызванную накоплением транков. GHC оценивает преобразователь только тогда, когда это необходимо. Мы спрашиваем, равно ли значение n 0 каждый раз до tri, поэтому в этом параметре нет наращивания переходов, но никому не нужно знать значение accum до самого конца, что может быть действительно огромным переходом, как вы можете видеть из пример. При оценке этого огромного блока стек может переполниться.

Решение состоит в том, чтобы заставить tri оценить accum раньше. Обычно это делается с помощью BangPattern (но можно сделать и с seq, если вам не нравятся расширения).

{-# LANGUAGE BangPatterns #-}
tri :: Int -> Int -> Int
tri 0 !accum = accum
tri n !accum = tri (n-1) (accum + n)

! перед accum означает "оценить этот параметр в момент сопоставления с шаблоном" (хотя шаблону технически не нужно знать его значение). Затем мы получаем эту оценочную трассировку:

tri 3 0
  = tri (3-1) (0+3)
  = tri 2 3          -- we evaluate 0+3 because of the bang pattern
  = tri (2-1) (3+2)
  = tri 1 5
  = tri (1-1) (5+1)
  = tri 0 6
  = 6

Надеюсь, это поможет.

person luqui    schedule 07.10.2016
comment
Спасибо за ваши усилия. Я читаю о BangPatterns. Однако я оставляю вопрос открытым в надежде получить ответ о части Common LISP. - person zweiblumen; 07.10.2016

О стиле:

(if (predicate? ...) t nil)

просто

(predicate? ...)

Вы проверяете со своим IF, если T равно T, а затем возвращаете T. Но T это уже T, так что можно просто вернуть.

person Rainer Joswig    schedule 07.10.2016
comment
Спасибо. Я вижу это сейчас. - person zweiblumen; 07.10.2016