Now that you know What Friendly Sam is for, let’s get started!

Variables and expressions

In Friendly Sam, each variable is an instance of the Variable class. Let’s create one:

>>> from friendlysam import Variable
>>> my_var = Variable('x')
>>> my_var
<friendlysam.opt.Variable at 0x...: x>
>>> print(my_var)
x

Variables can be added, multiplied, subtracted, and so on, to form expressions, including equalities and inequalities.

>>> expressions = [
...     my_var * 2 + 1,
...     (my_var + 1) * 2,
...     my_var * 2 <= 3
... ]
>>> for expr in expressions:
...     expr
<friendlysam.opt.Add at 0x...>
<friendlysam.opt.Mul at 0x...>
<friendlysam.opt.LessEqual at 0x...>
>>>
>>> for expr in expressions:
...     print(expr)
x * 2 + 1
(x + 1) * 2
x * 2 <= 3

Warning

The operator == is reserved for checking object similarity, just like we are used to in Python. To create the relation “x equals y”, use Eq:

>>> from friendlysam import Eq
>>> my_var == 1
False
>>> print(Eq(my_var, 1))
x == 1

There is also a nice Sum operation you should use for large sums. Using the built-in sum() will create a deeply nested and very inefficient tree of Add objects.

>>> from friendlysam import Sum
>>> many_terms = [my_var * i for i in range(100)]
>>> Sum(many_terms)
<friendlysam.opt.Sum at 0x...>
>>> sum(many_terms)
<friendlysam.opt.Add at 0x...>

Names don’t mean anything

In the example above, we named the Variable object 'x'. This is nothing more than a string attached to the object, and it does not say anything about the identity of the variable. In principle you can have several Variable objects with the same name, but that’s really confusing and should not be necessary.

>>> my_var = Variable('y')
>>> my_other_var = Variable('y')
>>> my_var == my_other_var
False
>>> print(my_var + my_other_var)
y + y

It is often a good idea to give your variables names you can recognize, because that simplifies debugging when you want to inspect the expressions you have made with the variables. But if you don’t want to name variables you don’t have to. The variables are then named automatically.

>>> Variable()
<friendlysam.opt.Variable at 0x...: x1>
>>> Variable()
<friendlysam.opt.Variable at 0x...: x2>

VariableCollection is like an indexed Variable

There is also a convenient class called VariableCollection. It is a sort of lazy dictionary, which creates variables when you ask for them:

>>> from friendlysam import VariableCollection
>>> z = VariableCollection('z')
>>> z
<friendlysam.opt.VariableCollection at 0x...: z>
>>> z(1)
<friendlysam.opt.Variable at 0x...: z(1)>
>>> z((1, 'a'))
<friendlysam.opt.Variable at 0x...: z((1, 'a'))>
>>> z(None)
<friendlysam.opt.Variable at 0x...: z(None)>

You can think of VariableCollection as an indexed variable, but all it really does is to create variables when you call it, and then remember them.

The index must be hashable. For example, tuples are valid indices, but not lists:

>>> z((3, 1, 4))
<friendlysam.opt.Variable at 0x...: z((3, 1, 4))>
>>> z([3, 1, 4])
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'

Variables can be named in a namespace, like this:

>>> from friendlysam import namespace
>>> with namespace('cheese'):
...     cheese1 = Variable('gorgonzola')
...     cheese2 = VariableCollection('ricotta')
...
>>> cheese1
<friendlysam.opt.Variable at 0x...: cheese.gorgonzola>
>>> cheese2
<friendlysam.opt.VariableCollection at 0x...: cheese.ricotta>

The namespace doesn’t affect the function of a variable in any way. It only prepends a string representation of whatever object to the variable name, so you can also do things like this:

>>> with namespace(dict()):
...     Variable('x')
...
<friendlysam.opt.Variable at 0x...: {}.x>

Variables can have values

You can assign a value to a variable. The variable will still work in expressions:

>>> x = Variable('x')
>>> x.value = 39
>>> expression = x + 3
>>> expression
<friendlysam.opt.Add at 0x...>
>>> print(expression)
x + 3

The difference is that you can now evaluate expressions. But note that the expression object is unchanged.

>>> float(expression)
42.0
>>> print(expression)
x + 3

You can change or delete the value:

>>> x.value = 0.5
>>> int(expression)
3
>>> float(expression)
3.5
>>> expression.value
3.5
>>> del x.value
>>> float(expression)
Traceback (most recent call last):
...
friendlysam.opt.NoValueError: cannot get a numeric value: x + 3 evaluates to x + 3

And it works for relations, too:

>>> x.value = 10
>>> (x <= 12).value
True

Expressions are immutable

Expressions are hashed by structure: If they do the same thing, they hash and compare equal. This also means they are considered equal e.g. as dict keys.

>>> expr1 = x * 2
>>> expr2 = x * 2
>>> expr1 is expr2 # Different objects!
False
>>> expr1 == expr2 # But similar
True
>>> d = dict()
>>> d[expr1] = 'some value'
>>> d[expr2]
'some value'

Expressions are immutable, meaning that their state can never be changed. In the example above, expr1 == expr2 and that will always be true. Two expressions are interchangeable if (and only if) they compare equal. For any purpose, in any situation, expr1 will always do the same thing as expr2.

However, as you saw above, the result of float(expr1) may vary depending on whether variables in the expression have values. Let’s look a little bit closer:

>>> x.value = 3
>>> expression = x + 39
>>> float(expression)
42.0
>>> x.value = 100
>>> another_expression = x + 39
>>> expression == another_expression
True
>>> float(expression)
139.0
>>> float(another_expression)
139.0

This is pretty much analogous to a tuple of mutable objects. The tuple itself may never change, but its contents may:

>>> a = [1, 2, 3]
>>> my_tuple = (a, 'something')
>>> my_tuple
([1, 2, 3], 'something')
>>> a[:] = ['changed'] # Only changing the contents of the list
>>> another_tuple = (a, 'something')
>>> my_tuple == another_tuple
True
>>> my_tuple
(['changed'], 'something')
>>> another_tuple
(['changed'], 'something')

Behind value is evaluate()

You might want to know what is happening behind the scenes when you ask for expression.value or float(expression). In that case, check out the method evaluate().