I have to write my own List.map function using 'for elem in list' and tail/non-tail recursive. I have been looking all around Google for some tips, but didn't find much. I am used to Python and it's pretty hard to not think about using its methods, but of course, these languages are very different from each other.
For the first one I started with something like:
let myMapFun funcx list =
for elem in list do
funcx elem::[]
Tail recursive:
let rec myMapFun2 f list =
let cons head tail = head :: tail
But anyway, I know it's wrong, it feels wrong. I think I am not used yet to F# strcture. Can anyone give me a hand?
Thanks.
As a general rule, when you're working through a list in F#, you want to write a recursive function that does something to the head of the list, then calls itself on the tail of the list. Like this:
// NON-tail-recursive version
let rec myListFun list =
match list with
| [] -> valueForEmptyList // Decision point 1
| head :: tail ->
let newHead = doSomethingWith head // Decision point 2
newHead :: (myListFun tail) // Return value might be different, too
There are two decisions you need to make: What do I do if the list is empty? And what do I do with each item in the list? For example, if the thing you're wanting to do is to count the number of items in the list, then your "value for empty list" is probably 0, and the thing you'll do with each item is to add 1 to the length. I.e.,
// NON-tail-recursive version of List.length
let rec myListLength list =
match list with
| [] -> 0 // Empty lists have length 0
| head :: tail ->
let headLength = 1 // The head is one item, so its "length" is 1
headLength + (myListLength tail)
But this function has a problem, because it will add a new recursive call to the stack for each item in the list. If the list is too long, the stack will overflow. The general pattern, when you're faced with recursive calls that aren't tail-recursive (like this one), is to change your recursive function around so that it takes an extra parameter that will be an "accumulator". So instead of passing a result back from the recursive function and THEN doing a calculation, you perform the calculation on the "accumulator" value, and then pass the new accumulator value to the recursive function in a truly tail-recursive call. Here's what that looks like for the myListLength
function:
let rec myListLength acc list =
match list with
| [] -> acc // Empty list means I've finished, so return the accumulated number
| head :: tail ->
let headLength = 1 // The head is one item, so its "length" is 1
myListLength (acc + headLength) tail
Now you'd call this as myListLength 0 list
. And since that's a bit annoying, you can "hide" the accumulator by making it a parameter in an "inner" function, whose definition is hidden inside myListLength
. Like this:
let myListLength list =
let rec innerFun acc list =
match list with
| [] -> acc // Empty list means I've finished, so return the accumulated number
| head :: tail ->
let headLength = 1 // The head is one item, so its "length" is 1
innerFun (acc + headLength) tail
innerFun 0 list
Notice how myListLength
is no longer recursive, and it only takes one parameter, the list whose length you want to count.
Now go back and look at the generic, NON-tail-recursive myListFun
that I presented in the first part of my answer. See how it corresponds to the myListLength
function? Well, its tail-recursive version also corresponds well to the tail-recursive version of myListLength
:
let myListFun list =
let rec innerFun acc list =
match list with
| [] -> acc // Decision point 1: return accumulated value, or do something more?
| head :: tail ->
let newHead = doSomethingWith head
innerFun (newHead :: acc) tail
innerFun [] list
... Except that if you write your map
function this way, you'll notice that it actually comes out reversed. The solution is to change innerFun [] list
in the last line to innerFun [] list |> List.rev
, but the reason why it comes out reversed is something that you'll benefit from working out for yourself, so I won't tell you unless you ask for help.
And now, by the way, you have the general pattern for doing all sorts of things with lists, recursively. Writing List.map
should be easy. For an extra challenge, try writing List.filter
next: it will use the same pattern.
let myMapFun funcx list =
[for elem in list -> funcx elem]
myMapFun ((+)1) [1;2;3]
let rec myMapFun2 f = function // [1]
| [] -> [] // [2]
| h::t -> (f h)::myMapFun f t // [3]
myMapFun2 ((+)1) [1;2;3] // [4]
let myMapFun3 f xs = // [6]
let rec g f xs= // [7]
match xs with // [1]
| [] -> [] // [2]
| h::t -> (f h)::g f t // [3]
g f xs
myMapFun3 ((+)1) [1;2;3] // [4]
// [5] see 6 for a comment on value Vs variable.
// [8] see 8 for a comment on the top down out-of-scopeness of F#
(* Reference:
convention: I've used a,b,c,etc refer to distinct aspects of the numbered reference
[1] roughly function is equivalent to the use of match. It's the way they do it in
OCaml. There is no "match" in OCaml. So this is a more compatible way
of writing functions. With function, and the style that is used here, we can shave
off a whole two lines from our definitions(!) Therefore, readability is increased(!)
If you end up writing many functions scrolling less to be on top
of the breadth of what is happening is more desirable than the
niceties of using match. "Match" can be
a more "rounded" form. Sometimes I've found a glitch with function.
I tend to change to match, when readability is better served.
It's a style thing.
[1b] when I discovered "function" in the F# compiler source code + it's prevalence in OCaml,
I was a little annoyed that it took so long to discover it + that it is deemed such an
underground, confusing and divisive tool by our esteemed F# brethren.
[1c] "function" is arguably more flexible. You can also slot it into pipelines really
quickly. Whereas match requires assignment or a variable name (perhaps an argument).
If you are into pipelines |> and <| (and cousins such as ||> etc), then you should
check it out.
[1d] on style, typically, (fun x->x) is the standard way, however, if you've ever
appreciated the way you can slot in functions from Seq, List, and Module, then it's
nice to skip the extra baggage. For me, function falls into this category.
[2a] "[]" is used in two ways, here. How annoying. Once it grows on you, it's cool.
Firstly [] is an empty list. Visually, it's a list without the stuff in it
(like [1;2;3], etc). Left of the "->" we're in the "pattern" part of the partern
matching expression. So, when the input to the function (lets call it "x" to stay
in tune with our earliest memories of maths or "math" classes) is an empty list,
follow the arrow and do the statement on the right.
Incidentally, sometimes it's really nice to skip the definition of x altogether.
Behold, the built in "id" identity function (essentially fun (x)->x -- ie. do nothing).
It's more useful than you realise, at first. I digress.
[2b] "[]" on the right of [] means return an empty list from this code block. Match or
function symantics being the expression "block" in this case. Block being the same
meaning as you'll have come across in other languages. The difference in F#, being
that there's *always* a return from any expression unless you return unit which is
defined as (). I digress, again.
[3a] "::" is the "cons" operator. Its history goes back a long way. F# really only
implements two such operators (the other being append @). These operators are
list specific.
[3b] on the lhs of "->" we have a pattern match on a list. So the first element
on the lhs of :: goes into the value (h)ead, and the rest of the list, the tail,
goes into the (t)ail value.
[3c] Head/tail use is very specific in F#. Another language that I like a lot, has
a nicer terminology for obviously interesting parts of a list, but, you know, it's
nice to go with an opinionated simplification, sometimes.
[3d] on the rhs of the "->", the "::", surprisingly, means join a single element
to a list. In this case, the result of the function f or funcx.
[3e] when we are talking about lists, specifically, we're talking about a linked
structure with pointers behind the scenes. All we have the power to do is to
follow the cotton thread of pointers from structure to structure. So, with a
simple "match" based device, we abstract away from the messy .Value and .Next()
operations you may have to use in other languages (or which get hidden inside
an enumerator -- it'd be nice to have these operators for Seq, too, but
a Sequence could be an infinite sequence, on purpose, so these decisions for
List make sense). It's all about increasing readability.
[3f] A list of "what". What it is is typically encoded into 't (or <T> in C#).
or also <T> in F#. Idiomatically, you tend to see 'someLowerCaseLetter in
F# a lot more. What can be nice is to pair such definitions (x:'x).
i.e. the value x which is of type 'x.
[4a] move verbosely, ((+)1) is equivilent to (fun x->x+1). We rely on partial
composition, here. Although "+" is an operator, it is firstmost, also a
function... and functions... you get the picture.
[4b] partial composition is a topic that is more useful than it sounds, too.
[5] value Vs variable. As an often stated goal, we aim to have values that
never ever change, because, when a value doesn't change, it's easier to
think and reason about. There are nice side-effects that flow from that
choice, that mean that threading and locking are a lot simpler. Now we
get into that "stateless" topic. More often than not, a value is all you
need. So, "value" it is for our cannon regarding sensible defaults.
A variable, implies, that it can be changed. Not strictly true, but in
the programming world this is the additional meaning that has been strapped
on to the notion of variable. Upon hearing the word variable, ones mind might
start jumping through the different kinds of variable "hoops". It's more stuff
that you need to hold in the context of your mind. Apparently, western people
are only able to hold about 7 things in their minds at once. Introduce mutability
and value in the same context, and there goes two slots. I'm told that more uniform
languages like Chinese allow you to hold up to 10 things in your mind at once.
I can't verify the latter. I have a language with warlike Saxon and elegant
French blended together to use (which I love for other reasons).
Anyway, when I hear "value", I feel peace. That can only mean one thing.
[6] this variation really only achieves hiding of the recursive function. Perhaps
it's nice to be a little terser inside the function, and more descriptive to
the outside world. Long names lead to bloat. Sometimes, it's just simpler.
[7a] type inference and recursion. F# is one of the nicest
languages that I've come across for elegantly dealing with recursive algorithms.
Initially, it's confusing, but once you get past that
[7b] If you are interested in solving real problems, forget about "tail"
recursion, for now. It's a cool compiler trick. When you get performance conscious,
or on a rainy day, it
might be a useful thing to look up.
Look it up by all means if you are curious, though. If you are writing recursive
stuff, just be aware that the compiler geeks have you covered (sometimes), and
that horrible "recursive" performance hole (that is often associated with
recursive techniques -- ie. perhaps avoid at all costs in ancient programming
history) may just be turned into a regular loop for you, gratis. This auto-to-loop
conversion has always been a compiler geek promise. You can rely on it more though.
It's more predictable in F# as to when "tail recursion" kicks in. I digress.
Step 1 correctly and elegantly solve useful problems.
Step 2 (or 3, etc) work out why the silicon is getting hot.
NB. depending on the context, performance may be an equally important thing
to think about. Many don't have that problem. Bear in mind that by writing
functionally, you are structuring solutions in such a way that they are
more easily streamlineable (in the cycling sense). So... it's okay not to
get caught in the weeds. Probably best for another discussion.
[8] on the way the file system is top down and the way code is top down.
From day one we are encouraged in an opinionated (some might say coerced) into
writing code that has flow + code that is readable and easier to navigate.
There are some nice side-effects from this friendly coercion.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With