Algorithms Illuminated in F#, the beginning
source link: https://www.cogitoergofun.io/algorithms-illuminated-in-fsharp/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Algorithms Illuminated in F#, the beginning
Implementing the Karatsuba fast multiplication algorithm in F#
Édgar Sánchez Gordón
Tim Roughgarden has a well-reputed course on algorithms, based on this course he wrote four books on the topic that are also pretty popular, and to be sure the videos are worth the watch. But the thing with most such courses and books is that they take a mostly imperative stance: an algorithm is a series of steps that change the state of variables or whole data structures, this doesn't bode well with the functional ideas of immutability and transforming functions; that got me thinking about how natural, or not, would it be to implement Tim's pseudo-code in a language like F#. I am clear that doing this will take time and effort but, on the other hand, I will have material for months, or rather quarters, of blog posts; furthermore, I am certain to refresh and learn a lot of stuff. For this series, I am assuming that you have got a basic knowledge of F# syntax, there are great resources for this, but aside from that I will try to explain most of the F# idioms and patterns that I will be using, so with that, let's get started!
The first algorithm that Tim discusses in Algorithms Illuminated, Part 1: The Basics is Karatsuba multiplication, a fast method for multiplying very large numbers (like with thousands of digits), by all means, I invite you to read Tim explanation but the gist of it is in this picture (kindly taken from the book):
Break the numbers to be multiplied in two pieces each (56, 78, 12, and 34 in the example) and solve the product in terms of operations with these smaller numbers according to this method (image also taken from the book):
As I mentioned above, it is better to go to the book (or the video) to understand why and how the algorithm works; once we get it, implementing this pseudo-code in F# is somewhat straightforward:
let rec karatsuba x y =
let (+) x y = add x y
let (-) x y = subtract x y
let (<<<) x n = leftShift x n
let xLength = List.length x
let yLength = List.length y
if yLength < 2 then
multByOneDigit x y[0]
elif xLength < 2 then
multByOneDigit y x[0]
else
let n2 = (min xLength yLength) / 2
let a, b = splitTail x n2
let c, d = splitTail y n2
let p = a + b
let q = c + d
let ac = karatsuba a c
let bd = karatsuba b d
let pq = karatsuba p q
let adbc = pq - ac - bd
(ac <<< n2*2) + (adbc <<< n2) + bd
First of all, let me state that the definition and use of the +
, -
, and <<<
operators are totally optional but it enhances the readability, e.g., the last line without the operators would be something like this: add (add (leftShift ac (n2*2)) (leftShift adbc n2)) bd
, certainly harder to digest. Besides that, the F# code is almost a line-by-line translation from the pseudo-code.
For this exercise, I have chosen to represent each number as a list of decimal digits so for example 5678 is [5; 6; 7; 8]
, I am fully aware that most implementations of Karatsuba multiplication use binary digits in a continuous memory block, but my main focus is the readability of the algorithm implementation, so I am happily leaving the fast, heavy-duty versions to the performance experts.
The base case of the recursive karatsuba
function is multiplying by one digit, e.g., 5678 x 4. We will follow the grade school method of going from the least to the most significant digit, an imperative implementation would be:
let multByOneDigit x d =
let mutable xByd = List.empty<int>
let mutable carry = 0
for i in List.length x - 1 .. -1 .. 0 do
let bruteProduct = x[i] * d
let newCarry, newDigit = bruteProduct / 10, bruteProduct % 10 + carry
xByd <- newDigit :: xByd
carry <- newCarry
if carry > 0 then carry :: xByd else xByd
This code proceeds from right to left multiplying each digit of x
by d
, successively growing the result list one digit at a time while carrying any value over 10 for the next step, for example, to multiply [5; 6; 7; 8]
by 2
it first does 8*2
generating a list with just [6]
and carrying 1
, then it does 7*2+1
generating a new list with [5; 6]
and carrying 1
again, and so on. Though more than one of my F# friends may sneer at the mutable variables and the state changes, there is nothing wrong with using imperative code in F#. But exploring a functional implementation of multiplyByOneDigit
, without variables, with emphasis on functions that transform data, is worth the effort. One functional version of multByOneDigit
could be:
let multByOneDigit (x: int list) d =
let rec multCurrentDigit i carry xByd =
if i < 0 then
if carry > 0 then carry :: xByd else xByd
else
let bruteProduct = x[i]*d
multCurrentDigit (i - 1) (bruteProduct / 10) (bruteProduct % 10 + carry :: xByd)
multCurrentDigit (List.length x - 1) 0 []
This follows the exact same procedure as the previous implementation but it uses a function that processes the i-th digit, does the multiplication, and passes the carry and the new list to a new call of itself to process the next left digit, for example, to multiply [5; 6; 7; 8]
by 2
it starts by calling multCurrentDigit 3 0 []
, which then makes the call multCurrentDigit 2 1 [6]
(2 x 8 = 16, so we keep the 6 and carry 1), then makes the call multCurrentDigit 1 1 [5; 6]
, and so on. If you picture the process in your head, this code processes a list from the tail to the head, does its calculation, and passes the carry and the growing result list to the next step. When you feel comfortable with the way this works, you can go one step further and express this using the List.foldback
function like this:
let multByOneDigit x d =
let lastCarry, xByd =
List.foldBack
(fun xi (carry, partialProduct) ->
let bruteProduct = xi*d
(bruteProduct / 10, bruteProduct % 10 + carry :: partialProduct) )
x (0, [])
if lastCarry > 0 then lastCarry :: xByd else xByd
To multiply [5; 6; 7; 8]
by 2
, List.foldback
starts by calling the lambda function like so fun 8 (0, [])
, note that List.foldback
takes just one parameter to accumulate and roll data to the next step but we trick it to pass two things by the simple means of passing a tuple, so for the next digit List.foldback
does the callfun 7 (1, [6])
, then fun 6 (1, [5; 6])
, and so on.
If that last piece of code strikes you as alien, you are in the exact same position I was a few years ago, it took me quite a while to get used to the functional style, that is why I think F#'s ability to also accommodate imperative code is great for people adopting this new paradigm. It is not unusual that you want to process a collection (list, array, or something similar) item by item, "growing" a resulting collection in each step, in such scenarios List.fold
or List.foldback
are handy tools, get used to them, in any case, you can always fall back to defining a recursive function that processes one item, accumulates the result, and finally calls itself for the next item, as we did in our first functional implementation of multByOneDigit
.
The next function in our implementation of Karatsuba multiplication is splitTail
that takes the list [5; 6; 7; 8]
and returns two lists: [5; 6]
and [7; 8]
, with the shiny new slicers in F# 6 we can do this in a cinch:
let splitTail x m =
let splitPoint = List.length x - m
x[.. splitPoint - 1], x[splitPoint ..]
Subtly, the last line returns a tuple with two lists, I have to say that more than functional this is almost declarative programming!
Now comes the time to implement the functions at the heart of Karatsuba: add
, subtract
, and leftShift
, but this post is growing big already so let's leave it for the next entry, it will be in just a couple of days, I promise!
PD. The title image is By Cmglee - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=78011434
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK