Схема / Лисп вложенные циклы и рекурсия - PullRequest
6 голосов
/ 01 ноября 2009

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

например. У меня есть два списка, которые я должен проверить на их декартовом произведении.

Как лучше всего подойти к этим проблемам? Любые указатели о том, как упростить эти типы функций?


Я немного уточню, так как мое намерение может быть недостаточно ясным.

Обычная рекурсивная функция может выглядеть так:

(define (factorial n)
  (factorial-impl n 1))

(define (factorial-impl n t)
  (if (eq? n 0)
      t
      (factorial-impl (- n 1) (* t n))))

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

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

Ответы [ 3 ]

13 голосов
/ 05 ноября 2009

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

Фактически, в схеме map принимает функцию «n-аргумент», а «n» перечисляет и вызывает функция для каждого соответствующего элемента каждого списка:

> (map * '(3 4 5) '(1 2 3))
(3 8 15)

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

; curry takes:
;  * a p-argument function AND
;  * n actual arguments,
; and returns a function requiring only (p-n) arguments
; where the first "n" arguments are already bound. A simple
; example
; (define add1 (curry + 1))
; (add1 3)
;  => 4
; Many other languages implicitly "curry" whenever you call
; a function with not enough arguments.
(define curry
    (lambda (f . c) (lambda x (apply f (append c x)))))

; take a list of tuples and an element, return another list
; with that element stitched on to each of the tuples:
; e.g.
; > (stitch '(1 2 3) 4)
; ((4 . 1) (4 . 2) (4 . 3))
(define stitch
    (lambda (tuples element)
        (map (curry cons element) tuples)))

; Flatten takes a list of lists and produces a single list
; e.g.
; > (flatten '((1 2) (3 4)))
; (1 2 3 4)
(define flatten
    (curry apply append))

; cartesian takes two lists and returns their cartesian product
; e.g.
; > (cartesian '(1 2 3) '(4 5))
; ((1 . 4) (1 . 5) (2 . 4) (2 . 5) (3 . 4) (3 . 5))
(define cartesian
    (lambda (l1 l2)
        (flatten (map (curry stitch l2) l1))))

; cartesian-lists takes a list of lists
; and returns a single list containing the cartesian product of all of the lists.
; We start with a list containing a single 'nil', so that we create a
; "list of lists" rather than a list of "tuples".

; The other interesting function we use here is "fold-right" (sometimes called
; "foldr" or "reduce" in other implementations). It can be used
; to collapse a list from right to left using some binary operation and an
; initial value.
; e.g.
; (fold-right cons '() '(1 2 3))
; is equivalent to
; ((cons 1 (cons 2 (cons 3 '())))
; In our case, we have a list of lists, and our binary operation is to get the
; "cartesian product" between each list.
(define cartesian-lists
    (lambda (lists)
        (fold-right cartesian '(()) lists)))

; cartesian-map takes a n-argument function and n lists
; and returns a single list containing the result of calling that
; n-argument function for each combination of elements in the list:
; > (cartesian-map list '(a b) '(c d e) '(f g))
; ((a c f) (a c g) (a d f) (a d g) (a e f) (a e g) (b c f)
;  (b c g) (b d f) (b d g) (b e f) (b e g))
(define cartesian-map
    (lambda (f . lists)
        (map (curry apply f) (cartesian-lists lists))))

Без всех комментариев и некоторого более компактного синтаксиса определения функции мы имеем:

(define (curry f . c) (lambda x (apply f (append c x))))
(define (stitch tuples element)
        (map (curry cons element) tuples))
(define flatten (curry apply append))
(define (cartesian l1 l2)
        (flatten (map (curry stitch l2) l1)))
(define cartesian-lists (curry fold-right cartesian '(()))))
(define (cartesian-map f . lists)
        (map (curry apply f) (cartesian-lists lists)))

Я думал, что вышеупомянутое было достаточно «изящным» ... пока кто-то не показал мне эквивалентное определение Хаскелла:

cartes f (a:b:[]) = [ f x y | x <- a , y <- b ] 
cartes f (a:b:bs) = cartes f ([ f x y | x <- a , y <- b ]:bs) 

2 строки !!!

Я не настолько уверен в эффективности моей реализации - в частности, шаг "flatten" был написан быстро, но в итоге мог вызвать "append" с очень большим количеством списков, которые могут или не могут быть очень эффективными на некоторых схемах Реализации.

Для максимальной практичности / полезности вам нужна версия, которая может принимать "лениво оцененные" списки / потоки / итераторы, а не полностью определенные списки .... если хотите, функцию "декартово-картографический поток", которая бы тогда вернуть "поток" результатов ... но это зависит от контекста (я имею в виду концепцию "потока", представленного в SICP) ... и выйдет бесплатно из версии на Haskell благодаря ее ленивой оценке.

В общем, в Схеме, если вы хотите «разорвать» цикл в какой-то момент, вы также можете использовать продолжение (например, выбрасывание исключения, но это принято в Схеме для потока управления).

Мне было весело писать это!

2 голосов
/ 06 ноября 2009

Здесь уже есть несколько хороших ответов, но для простых вложенных функций (таких как хвостовой рекурсивный факториал) я предпочитаю именованное let:

(define factorial  
  (lambda (n)
    (let factorial-impl ([n n] [t 1])
      (if (eq? n 0)
        t
        (factorial-impl (- n 1) (* t n))))))
2 голосов
/ 02 ноября 2009

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

Например, в этом случае:

;compute the list of the (x,y) for y in l
(define (pairs x l)
  (define (aux accu x l)
    (if (null? l)
        accu
        (let ((y (car l))
              (tail (cdr l)))
          (aux (cons (cons x y) accu) x tail))))
  (aux '() x l))

(define (cartesian-product l m)   
  (define (aux accu l)
    (if (null? l) 
        accu
        (let ((x (car l)) 
              (tail (cdr l)))
          (aux (append (pairs x m) accu) tail))))
  (aux '() l))       

Вы определяете различные шаги: чтобы получить декартово произведение, если вы «перебираете» первый список, вам нужно будет иметь возможность вычислить список (x,y) для y в второй список.

...