Expr: The Lazy Expression#

Code readability is central to excelbird’s design. Therefore, we’ve given you the tools needed to let you build an entire workbook (and some of its logic) in a single line of code, without creating any variables other than your input data. This is great. It means your Book layout can be read the way its output will look: in sequential order. The logic, references, and design features in an element can be isolated to that element’s location in your layout.

To accomplish this, we must be able to reference elements that either aren’t assigned to variables, or may not even exist yet. For instance, say you want to design a Sheet that has a table at the bottom, and some summary statistics at the top that reference columns in the table. Normally, you’d need to create the table first, and then declare the summary section which reference it. Then, your code reads backwards. That’s no fun.

Before any explanation, here’s an example of how this is solved in excelbird.

Cell A1 in Sheet1 will read =A2 + B2 + Sheet2!A1:

Book(
    Sheet(
        Cell(ex="[one] + [two] + [three]"),
        Row(
            Cell(1, id="one"),
            Cell(2, id="two"),
        )
    ),
    Sheet(
        Cell(3, id="three")
    ),
).write()

Global Identifiers and Lazy Evaluation#

Every excelbird object has an id property (some have a header too) which, when set, saves a pointer to itself in memory, so it can be found from anywhere else in the Book.

As explained before, the whole Book layout is lazily evaluated - triggered only when .write() is called. This means that when you set an element’s identifier has no effect on when you can reference it.

This solves the problem identified earlier: You can arrange a summary card that sums/averages some columns in a table that will be arranged later, without creating variables for the summary card or table.

The ‘Expr’ Element#

Python has a builtin function eval() which takes a string and executes it as code: eval("1 + my_variable") is the same as 1 + my_variable.

xb.Expr works just like eval, but instead of real variable names, you’ll reference the id or header of an element in your layout, and it will wait until each reference is found before executing.

If you would normally say,

my_cell = Cell(1)
other = my_cell + 5

You can instead write,

my_cell = Cell(1, id="my_cells_id")
other = Expr("[my_cells_id] + 5")

The difference? Order doesn’t matter anymore

other = Expr("[my_cells_id] + 5")
my_cell = Cell(1, id="my_cells_id")

Another difference? You no longer need to reference the my_cell variable

Book(
    Sheet(
        Expr("[my_cells_id] + 5"),
        Cell(1, id="my_cells_id"),
    )
).write()

This code can still be improved: it’s not clear to the reader which type will be returned when the Expr executes. Instead…

Book(
    Sheet(
        Cell(ex="[my_cells_id] + 5"),
        Cell(1, id="my_cells_id"),
    )
).write()

The ex argument was intercepted before Cell’s constructor could be called, and an Expr was returned instead

Not only is this more readable, but the returned Expr has been told that it’s supposed to return a Cell when it executes, and if it doesn’t (perhaps my_cells_id is actually a column header), a descriptive error will be raised to help you fix the problem

Danger

Expr creation by use of the ex argument is only recommended when creating the element from inside your layout, as shown above, rather than by assigning to a variable. The ability to intercept arguments like ex and return an instance of a different class is an unusual strategy, and can cause serious confusion for a reader who sees a variable assigned to a certain type, only to find out it’s a different type. It’s justified in the use-case above because if no variable is declared, there’s no way to interact with the element directly, and thus no chance of confusion.