Functions: How Sophie Calculates
Super-Fancy Calculator
Here’s a small program showing how math (and comments) in Sophie appears:
begin:
3 + 4 * 5; # Precedence works like you learned in school.
( 3 + 4 ) * 5; # You can always override that with parentheses
sqrt(1+1); # A number of mathematical functions and constants are built-in.
end.
You can’t miss the explanatory text on each line.
Sophie sees the # mark and concludes that the remainder of that line is a comment.
(This is a fairly typical convention.)
Comments are great for telling people about your code, for Sophie ignores them.
Let’s break down the meaning of each expression, line-by-line:
3 + 4 * 5: Produces23because multiplication comes before addition.( 3 + 4 ) * 5: Produces35because parenthesis.sqrt(1+1): Produces1.4142135623730951. The namesqrtrefers to a pre-defined function which computes the square-root of what you give it. This expression means to apply the number2to the function calledsqrtand use the result. Some functions take more than one input value: just put a comma (,) between each parameter.
You can see more examples in the some_arithmetic.sg example file.
- Exercise:
Find the
some_arithmetic.sgexample file. Read it (say, in notepad or textedit) and run it through the Sophie interpreter. This should be very similar to running thehello_world.sgexample. What does this say about how Sophie reads mathematical expressions?
Important
Sophie will only run a program she can read and understand completely in advance. Otherwise you’ll get a diagnostic message to try to help you sort out what went wrong. These messages are not in their final form, but they should at least pinpoint the issues.
- Exercise:
Modify the
some_arithmetic.sgexample file and save your changes, then try to run the modified version. What happens if you leave out a closing parenthesis, or leave out an operator between numbers? Can you make sense of the diagnostic messages?
Define your own!
Functions are the backbone of programming. Indeed, all of computing amounts to evaluating functions of varying complexity. So it’s time to talk about how to make and use them.
The usual standard explanation would begin something like this:
define:
double(x) = x + x;
square(x) = x * x;
area_of_rectangle(length, width) = length * width;
five = 5;
begin:
double(five); # 10
square(five); # 25
double(square(five)); # 50
square(double(five)); # 100
area_of_rectangle(20, 30); # 600
end.
We have here a simple Sophie program that defines three functions, called double, square, and area_of_rectangle.
It also defines a constant, called five, which conveniently enough refers to the number 5.
If you’re curious (and I hope you are) you can run it like:
D:\GitHub\sophie>sophie examples\tutorial\define_functions.sg
10
25
50
100
600
Let’s break this down:
In Sophie, the definitions of functions (and constants) go in a section introduced with
define:.In this program, the names
x,length, andwidthserve as formal parameters. That means thexindouble(x)is a place-holder for whatever other actual value. Same forlengthandwidthinarea_of_rectangle.When you want to write a function of more than one parameter, separate them by a comma.
You can, of course, refer to functions from within functions.
Names are Important
Consider the implications if five were instead called six in a large program:
People might look at the word six and mistakenly guess that it would mean 6,
as it would in a sane world.
This sort of treachery is typically called unmaintainable by those in the business, but I have a better word for it: unethical. Don’t do it. Pick names that evoke the proper meaning. If the meaning is abstract, pick an abstract name. The most abstract names of all are single letters near the end of the alphabet.
More Fun with Functions
You can do quite a bit with functions. Consider this example:
# Simplistic demonstration of Newton's Method as an iterated function.
define:
iterate_four_times(fn, x) = fn( fn( fn( fn( x ) ) ) );
root(square) = iterate_four_times(newton, 1) where
newton(guess) = (guess + square/guess) / 2;
end root;
begin:
root(2); # 1.4142135623746899 -- good to 13 digits!
# Exact value is 1.4142135623730951
root(17); # 4.126106627581331 -- Only the first three digits are correct,
# Exact value is 4.123105625617661 -- but it's all downhill from there.
end.
This program illustrates Isaac Newton’s method for figuring square-roots. The method achieves excellent accuracy after just a few steps if you start with a decent guess. (Start with a bad guess, and it takes a few extra steps. Selecting good guesses is a topic for another time.)
Once again, let’s study the bits.
iterate_four_timesis a function which takes a function as one of its parameters. The body expression is to call that function on the result of… well, you get the point. (One convention to make this scenario clear is visible in how the parameters are named:fnis commonly the name of a function. Similarly,xis often a number.)The first key point about
rootis thewhereclause. It allows you nest functions within functions (within functions… etc.). In this case,newtonis defined withinroot. That’s useful for two things:First, it hides the internals. If
newtonis only relevant toroot, then onlyrootneeds to seenewton. This is a good way to limit the amount of information you need to keep in your head at once.Second, it allows
newtonto see values that only exist within the context ofroot. Specifically,newtoncan use the value ofsquareeven wheniterate_four_timescalls it. This phenomenon is called closure.