Tracks
/
Factor
Factor
/
Exercises
/
Lasagna Luminary
Lasagna Luminary

Lasagna Luminary

Learning Exercise

Introduction

Stack juggling is fine for short words. When a word has several inputs and you need to refer to them by name, Factor offers locals — named bindings introduced with :: (for word definitions) or [| | ] (for inline lambdas).

:: ( inputs -- outputs )

Replace : with :: and the input names become local variables in the body:

USING: locals ;

:: hypotenuse ( a b -- c )
    a a *
    b b *
    + sqrt ;

Compare with the stack-shuffling version:

! DOCTEST: SKIP   (shown for comparison — would re-define hypotenuse)
: hypotenuse ( a b -- c )
    [ sq ] bi@ + sqrt ;

Both are valid — locals shine when an input is referenced more than once or when the order of operations doesn't naturally line up with stack order.

Binding intermediate values with :>

Inside a :: body (or an inline [| ... |] lambda), :> pops a value off the data stack and binds it to a fresh local:

value :> name      ! immutable binding
value :> name!     ! mutable binding (name! is its setter)

The immutable form is the common one — use it to name an intermediate so the body reads top-to-bottom:

USING: locals math ;

:: average-3 ( x y z -- avg )
    x y + z + :> sum
    sum 3 / ;

The ! flavour declares the local as mutable; later in the body, value name! (no colon, no :>) reassigns it:

USING: kernel locals math math.order ;

:: clamped ( x lo hi -- y )
    x :> v!
    v lo < [ lo v! ] when
    v hi > [ hi v! ] when
    v ;

Mutable locals shine inside the iteration words you'll meet in later exercises (e.g. while).

[let — a scope from anywhere

:> only works inside a lexical scope, which :: and [| ... |] create automatically. To introduce locals from a plain : word or at the listener, wrap the code in [let ... ]:

[let code :> name code :> name ... body ]

Each :> name binds the previous expression's top-of-stack value; the bindings are visible to the rest of the form.

USING: locals math ;

[let 3 :> x 4 :> y x x * y y * + ] .   ! => 25

[let is the bridge between stack-style : definitions and named bindings — handy when only a slice of a word benefits from locals.

Lambdas — [| inputs | body ]

Inside a quotation, [| inputs | body ] introduces locals that the body can refer to by name. This is especially useful with higher-order words like assoc-map, whose quotation receives key and value:

USING: assocs locals ;

H{ { "a" 1 } { "b" 2 } }
[| key value | key value neg ] assoc-map .
! => H{ { "a" -1 } { "b" -2 } }

The same shape works for map-index (in [sequences][sequences]), whose quotation receives the element and its index. The names make clear which operand plays which role:

USING: locals sequences ;

! Divide each element by its 1-based position:
{ 10 80 270 } [| elt i | elt i 1 + / ] map-index .
! => { 10 40 90 }

The lambda's stack effect is determined by the input list and what its body produces.

When to use locals

Reach for locals when:

  • The same input is used in more than one place.
  • The natural order of operations doesn't match stack order, and the dup/swap/rot to fix it is hard to read.
  • You're inside an inline lambda passed to a higher-order word that hands you several values at once (as in assoc-map).

When the body is a clean linear flow, plain : definitions stay shorter and at least as readable.

assoc-map — one entry at a time

assoc-map (in assocs) maps a quotation over each entry of an associative collection:

assoc-map ( assoc quot: ( key value -- newkey newvalue ) -- newassoc )

A locals lambda is the cleanest way to write the quotation.

cond revisited

When you need to dispatch on f or on a numeric value, cond is exactly the right shape — no case needed because there's no single value to compare against.

Instructions

You're back in the kitchen, refining the lasagna recipe from Leah's Luscious Lasagna. The tasks here ask you to juggle several inputs at once, which is exactly when locals — Factor's named bindings — earn their keep.

1. Determine the cooking status

Define cooking-status to take a timer reading off the stack and return a status string.

  • 0 → "Lasagna is done."
  • f (the timer was never set) → "You forgot to set the timer."
  • any other number → "Not done, please wait."
12 cooking-status .   ! => "Not done, please wait."
0 cooking-status .    ! => "Lasagna is done."
f cooking-status .    ! => "You forgot to set the timer."

2. Estimate the preparation time

Define preparation-time to take an array of layer names and an average number of minutes per layer, and return the total preparation time.

{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } 3 preparation-time .
! => 18

3. Compute the noodles and sauce needed

Define quantities to take an array of layer names and return two values: the grams of noodles needed (50 g per noodle layer) and the litres of sauce needed (1/5 litre per sauce layer).

{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } quantities .s
! => 100
! => 2/5

4. Add the secret ingredient

Your friend sends a list of ingredients; the last item is their secret. Define add-secret-ingredient to take their list and yours and return your list with the secret appended.

{ "noodles" "sauce" "mozzarella" "kampot pepper" }
{ "noodles" "meat" "sauce" "mozzarella" }
add-secret-ingredient .
! => { "noodles" "meat" "sauce" "mozzarella" "kampot pepper" }

5. Scale the recipe

The cookbook recipe yields two portions. Define scale-recipe to take a recipe (a hashtable from ingredient name to amount) and a target number of portions, and return a new recipe scaled appropriately.

H{ { "noodles" 200 } { "sauce" 1/2 } { "mozzarella" 1 } { "meat" 100 } }
4 scale-recipe .
! => H{ { "noodles" 400 } { "sauce" 1 } { "mozzarella" 2 } { "meat" 200 } }
Edit via GitHub The link opens in a new window or tab
Factor Exercism

Ready to start Lasagna Luminary?

Sign up to Exercism to learn and master Factor with 47 concepts163 exercises, and real human mentoring, all for free.