Lesson 10 – Finally, FUNctional Programming

Introduction

Today we’re going to build something that might actually be of use to you in your daily life, a length measurement converter. We’ll add a few new tools, but also we’ll revisit many of the things we’ve learned along the way, including maps, arms of a core, and casting our results. This generator may look long, but it’s not as complicated as it seems. Let’s get started, then.

Goal

Write a generator to take a input measurement unit, an @rs value, and an output unit to which we will convert the input measurement. For example, this generator could convert a number of imperial feet to metric decameters.

Rune list

?&

(wutpam)
A conditional that checks a list of hoon conditions and results in %.y if both/all of them are true, and otherwise results in a %.n

Exercise

Try this in dojo:

?&(=(1 1) =(2 2))
%.y
?&(=(1 2) =(2 2))
%.n
?&(=(1 1) =(2 3))
%.n

~|(“tape” !!)

(sigbar and zapzap, respectively)
A stack tracing printf producing an error message, then crashing (!!). This allows you to give a human legible error to your user if the program fails for some reason

Exercise

Try this in dojo:

~|(“error” !!)
“error”
ford: %ride failed to execute:

+$

(lusbuc)
Defines a type using one of the buc runes - creation of custom types. This is difficult to demonstrate in dojo, but it will make sense in our example (we promise!).

@rs

Atomic type for single-precision floating point numbers. All atoms in hoon are fundementally integers, however Auras allow us to encode text, dates, and other information as atoms. A single-precision floating point uses 32 bits to represent a decimal number. An @rs literal always begins with a .
The literal

Exercise

Try this in dojo:

`@rs`.1.337
.1.337

(note: this is the @rs form of 1.337 -- it is important to remember the leading . requires for the literal. Note also that you can optionally specify an exponent as below:

.1.1e-1
.1.1e-1

(note: this is the equivalent of .11 in human legible numbers)

Generator


!.
|=  [fr-meas=@tas num=@rs to-meas=@tas]
=<
^-  @rs
?.  (check fr-meas to-meas)
  ~|("Invalid Measures" !!)
(output (meters fr-meas num) to-meas)
:: 
|%
+$  allowed  ?(%inch %foot %yard %furlong %chain %link %rod %fathom %shackle %cable %nautical-mile %hand %span %cubit %ell %bolt %league %megalithic-yard %smoot %barleycorn %poppy-seed %atto %femto %pico %nano %micro %milli %centi %deci %meter %deca %hecto %kilo %mega %giga %tera %peta %exa)
:: 
++  check
  |=  [fr-meas=@tas to-meas=@tas]
  &(?=(allowed fr-meas) ?=(allowed to-meas))
:: 
++  meters
  |=  [in=@tas value=@rs]
  =/  factor-one
    (~(got by convert-to-map) in)
  (mul:rs value factor-one)
:: 
++  output
  |=  [in=@rs out=@tas]
  ?:  =(out %meter)
    in
  (div:rs in (~(got by convert-to-map) out))
:: 
++  convert-to-map
  ^-  (map @tas @rs)
  %-  my
  :~  :-  %atto             .1e-18
      :-  %femto            .1e-15
      :-  %pico             .1e-12
      :-  %nano             .1e-8
      :-  %micro            .1e-6
      :-  %milli            .1e-3
      :-  %poppy-seed       .2.212e-2
      :-  %barleycorn       .8.47e-2
      :-  %centi            .1e-2
      :-  %inch             .2.54e-2
      :-  %deci             .1e-1
      :-  %hand             .1.016e-1
      :-  %link             .2.012e-1
      :-  %span             .2.228e-1
      :-  %foot             .3.048e-1
      :-  %cubit            .4.472e-1
      :-  %megalithic-yard  .8.291e-1
      :-  %yard             .9.145e-1
      :-  %ell              .1.143
      :-  %smoot            .1.7
      :-  %fathom           .1.83
      :-  %rod              .5.03
      :-  %deca             .1e1
      :-  %chain            .2.012e1
      :-  %shackle          .2.743e1
      :-  %bolt             .3.658e1
      :-  %hecto            .1e2
      :-  %cable            .1.8532e2
      :-  %furlong          .2.0117e2
      :-  %kilo             .1e3
      :-  %mile             .1.609e3
      :-  %nautical-mile    .1.850e3
      :-  %league           .4.830e3
      :-  %mega             .1e6
      :-  %giga             .1e8
      :-  %tera             .1e12
      :-  %peta             .1e15
      :-  %exa              .1e18
      :-  %meter            .1
    ==
  --

Walkthrough

  • !.
    !. disables stack tracing for the following expression. We use it here since we've already written an error message for the one error case.

    • |=
      |= Creates a gate, taking two children -- a sample and some hoon to evaluate.

      • [fr-meas=@tas num=@rs to-meas=@tas]
        The first child of |=, our type specification for our sample, a cell of [@tas @rs @tas]
      • The second child of |= is the following expression, which includes the rest of our code:
        =<
        =< inverts two hoon statements, and evaluates the first in light of the second. This is necessary in order to have our |% core in the subject.

        • ^-
          • @rs

            ^- casts our results as an @rs - we will be converting one measure to another using fractional values where appropriate, so we must use @rs (or another floating point type)

          • ?. Conditionally branch based on the true/false value of the first child hoon.
            • (check fr-meas to-meas) Use the ++check arm to verify that our input and output units are valid.
            • ~|("Invalid Measures" !!) If ++check returns false, crash with error message.
              Note that since we used !. above, the rest of the stack trace will not be printed.
            • (output (meters fr-meas num) to-meas) Call the ++output arm, which takes two children.
              The first child must be a value in meters, which is exactly what is returned by (meters fr-meas num). The second child is the desired output unitto-meas.

          This entire expression is the first child of =<. It will be evaluated with the product of the following expression taken as the subject.

        • |%
          |% creates a core with a number of arms and is terminated by the boundary - -.

          • +$  allowed+$ indicates a type-constructing arm named allowed
            • ?(%inch %foot %yard %furlong %chain %link %rod %fathom %shackle %cable %nautical-mile %hand %span %cubit %ell %bolt %league %megalithic-yard %smoot %barleycorn %poppy-seed %atto %femto %pico %nano %micro %milli %centi %deci %meter %deca %hecto %kilo %mega %giga %tera %peta %exa)Our type, allowed, is defined as the union of the types above.
          • ++  check++ marks an arm named check.
            • |= |= creates a gate and takes two children.
              • [fr-meas=@tas to-meas=@tas] The first child of |= specifies the sample type as a cell of [@tas @tas] These are our input unit and output unit.
              • &(?=(allowed fr-meas) ?=(allowed to-meas)) If both fr-meas and to-meas nest under allowed, return true. Otherwise, return false.
          • ++  meters++ marks an arm named meters.
            • |= |= creates a gate and takes two children.
              • [in=@tas value=@rs]The first child of |= specifies the sample type as a cell of[@tas @rs]
              • =/ Pin a noun to the subject
                • factor-one with face factor-one
                • (~(got by convert-to-map) in) and value produced by looking up our input type in in our map. Recall from our last lesson that this is the irregular form of %~ calling the arm got of the door by
                • (mul:rs value factor-one) Use the Standard Library function mul:rs (note this is different than the multiply function used for @uds) to multiply the incoming @rs from the gate’s sample and the @rs found by factor-one’s map search.
                  The purpose of this arm is to convert our incoming measurement into meters, which is our standard unit, before converting to our desired output unit.
          • ++  output ++ marks an arm named output.
            • |= |= creates a gate and takes two children.
              • [in=@rs out=@tas]
                Bartis creates a gate that takes a cell of two arguments, an @rs and an @tas
              • ?: Branch based on the value of the first child.
                • =(out %meter) If our output type is %meter:
                • in Return in (which is our value, already in meters).
                • (div:rs in (~(got by convert-to-map) out)) Otherwise, divide in by the conversion factor obtained by map lookup of our output type.
          • ++  convert-to-map++ marks an arm named convert-to-map.
            • ^- Takes two children, a type and some hoon.
              • (map @tas @rs) Our type specification, a map of @tas and @rs pairs.
              • %-Call the gate my, which takes a null-terminated cell of ordered pairs and produces a map.
                • my
                • :~ Construct a null-terminated list.
                    • :- %atto .1e-18
                    • :- %femto .1e-15
                    • ...
                    • :- %exa .1e18
                    • :- %meter .1
                    • ==The == boundary closes :~.
          • - - The - - boundary closes our |% core.

Flow

  1. Request an input in the form of a cell
    |= [fr-meas=@tas num=@rs to-meas=@tas]
  2. Evaluate the next complete hoon expression in light of that following it
    =<
  3. Cast the output as an @rs
    ^- @rs
  4. Check whether both fr-meas and to-meas are valid measures using the arm check of the core below, and if they are proceed to evaluate the arm “output” using the values resulting from (meters fr-meas num) and to-meas, else crash with “Invalid Measures”
    ?.  (check fr-meas to-meas)
      ~|("Invalid Measures" !!)
    (output (meters fr-meas num) to-meas)
  5. Form a core
    |%
  6. Create a type called “allowed” that is a list of all valid measurements that we can convert between
    +$ allowed ?(%inch %foot %yard %furlong %chain %link %rod %fathom %shackle %cable %nautical-mile %hand %span %cubit %ell %bolt %league %megalithic-yard %smoot %barleycorn %poppy-seed %atto %femto %pico %nano %micro %milli %centi %deci %meter %deca %hecto %kilo %mega %giga %tera %peta %exa)
  7. When called at step 4, assist by taking in two @tas values and check to make sure both of them are in the “allowed” type established in step 6.
    ++ check
    |=  [fr-meas=@tas to-meas=@tas]
    &(?=(allowed fr-meas) ?=(allowed to-meas))
  8. Convert the “fr-meas” units into meters by multiplying the incoming @rs by the conversion factor from the map
    ++  meters
    |=  [in=@tas value=@rs]
    =/  factor-one
      (~(got by convert-to-map) in)
    (mul:rs value factor-one)
  9. Convert the meters from step 8 into the output “to-meas” units by either printing where to-meas is %meters, or dividing the value generated in Step 8 by the conversion factor
    ++ output
    |=  [in=@rs out=@tas]
    ?:  =(out %meter)
      in
    (div:rs in (~(got by convert-to-map) out))
  10. A map of conversion factors - generate this map for comparison in Steps 8 and 9
    ++ convert-to-map
      ^-  (map @tas @rs)
      %-  my
      :~  :-  %atto .1e-18
          :-  %femto  .1e-15
          :-  %pico  .1e-12
          :-  %nano  .1e-8
    ::      ...
          :-  %tera  .1e12
          :-  %peta  .1e15
          :-  %exa  .1e18
          :-  %meter  .1
      ==
    --

Readings


Magic 8-Ball - https://urbit.org/docs/tutorials/hoon/workbook/eightball/

    NOTE: The magic 8-ball employs a generator of a type we’ve not seen, called a %say generator. The first 2 lines and the 4th line are boilerplate, and the 3rd line brings in some “context” of entropy to support randomization. Don’t worry too much about this but try to understand how the rest of the generator works.

2.4 Standard Library: Trees, Sets and Maps - https://urbit.org/docs/tutorials/hoon/trees-sets-and-maps/

    Read the Maps section - feel free to read the other sections in addition, but the Maps section in particular

Homework

  • Add to this generator the ability to convert some other measurement (volume, mass, force, or another of your choosing).
    • Add an argument to the cell required by the gate that indicates whether the measurements are distance or your new measurement
    • Enforce strictly that the fr-meas and to-meas values are either lengths or your type
    • Create a new map of conversion values to handle your new measurement conversion method