From: Brian Hurt <brian.hurt@qlogic.com>
To: Richard Jones <rich@annexia.org>
Cc: <caml-list@inria.fr>
Subject: Re: [Caml-list] How to find out if a function is tail recursive?
Date: Thu, 12 Jun 2003 10:33:52 -0500 (CDT) [thread overview]
Message-ID: <Pine.LNX.4.33.0306121004140.14386-100000@eagle.ancor.com> (raw)
In-Reply-To: <20030612141549.GA14762@redhat.com>
On Thu, 12 Jun 2003, Richard Jones wrote:
> I was writing the section on tail recursion in the OCaml tutorial, and
> was surprised to find out that the range function (below) isn't tail
> recursive. Or at least it causes a stack overflow on a
> large-but-not-unreasonable input value.
>
> let rec range a b =
> if a > b then []
> else a :: range (a+1) b
> ;;
>
> let list = range 1 1000000;;
>
> Printf.printf "length = %d\n" (List.length list);;
>
> Can you tell me why this function isn't tail recursive, and share any
> useful tips on how to tell whether a function is or is not tail
> recursive?
A function is tail recursive if it fits both of the following criteria:
1) It returns the value returned by calling itself *unmodified*. And
modification of the return value means the functions is not tail
recursive. This is where your example fails- it modifies the return value
br list-prepending a new item to it. Note that even the simpliest
"modifications" defeat tail recursion. For example, the following code is
*not* tail recursive:
let rec sum x = if x > 1 then x + (sum (x-1)) else x
Trying to calculate sum 80000 overflows the stack. The general pattern
for how to fix this is to instead accumulate the modications into a
parameter, generally called 'accu' or 'accum' or similiar. Often times,
the recursion is then hidden in an inner function. So you might rewrite
the above function like:
let sum x =
let rec loop i accum =
if (i > 1) then
loop (i-1) (i + accum) (* <-- note this line! *)
else
i + accum
in
loop x 0
So now sum 80000 correctly returns 1052556352. With lists, we can only
build lists backwards, not forwards. So the general pattern is to build
the list backwards, then reverse it at the last moment. So you're example
might be written like:
let range a b =
let rec loop i accum =
if i > b then accum
else loop (i + 1) (i :: accum)
in
List.rev (loop a [])
Note, we can put the list reversal either outside of the loop, or inside
of the loop just before we exit, like:
let range a b =
let rec loop i accum =
if i > b then (List.rev accum)
else loop (i + 1) (i :: accum)
in
loop a []
There's really not any difference between the two.
2) The recursive call is not within a try/with block.
So, let's consider the "classic" problem- reading all the lines of a file
into a list of strings. The naive solution to this is:
let rec read_file chan =
try
let line = input_line chan in
line :: (read_file chan)
with
End_of_file -> []
The line that reads "line :: (read_file chan)" is not tail recursive-
we're modifying our return value (by prepending the current line onto it).
This violates condition #1. So we apply the normal pattern to it, and get
try #2:
let read_file chan =
let rec loop accum =
try
let line = input_line chan in
loop (line :: accum)
with
End_of_file -> List.rev accum
in
loop []
So now we're not modifying the return value. Instead of prepending the
new list element to the return value, we're prepending it to the
accumulator. So we're no longer violating condition #1. But we're still
violating condition #2- the recursion is still within a try/with block.
The try/with block really only needs to surround the input_line call- but
it also governs wether we exit the recursion or not. So my normal pattern
for solving this is to set a boolean variable as to wether we have data or
not. So the code now looks like:
let read_file chan =
let rec loop accum =
let line, eof =
try
(input_line chan), false
with
End_of_file -> "", true
in
if eof then
loop (line :: accum); (* <-- note, recursion now outside *)
else
List.rev accum
in
loop []
Now the recursive call is outside the try/with block. *And*, as we're not
modifying the return value at all, we're now tail recursive.
I'm probably missing more constraints, but it's pre-caffiene. I'll look
at this again later.
Brian
-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners
prev parent reply other threads:[~2003-06-12 15:15 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2003-06-12 14:15 Richard Jones
2003-06-12 14:31 ` Paul Steckler
2003-06-12 14:43 ` Luc Maranget
2003-06-12 16:15 ` John Max Skaller
2003-06-12 16:37 ` John Max Skaller
2003-06-13 7:23 ` [Caml-list] This is so nice John Max Skaller
2003-06-12 14:37 ` [Caml-list] How to find out if a function is tail recursive? Wolfgang Müller
2003-06-12 14:49 ` Neel Krishnaswami
2003-06-12 15:14 ` Chris Uzdavinis
2003-06-12 15:33 ` Brian Hurt [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=Pine.LNX.4.33.0306121004140.14386-100000@eagle.ancor.com \
--to=brian.hurt@qlogic.com \
--cc=caml-list@inria.fr \
--cc=rich@annexia.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox