[erlang-questions] how do I make print_area work

Garrett Smith g@REDACTED
Tue Aug 11 14:42:04 CEST 2015


Hi Richard,

On Mon, Aug 10, 2015 at 10:15 PM, Richard A. O'Keefe <ok@REDACTED> wrote:
> Once again I'm going to focus on a peripheral issue,
> which is actually quite important.

I was secretly hoping to get a critique from you - one of the benefits
of any given post :)

>> area() ->
>>    Shape = shape_from_user(),
>>    {X, Y} = dimensions_from_user(Shape),
>>    geom:area(Shape, X, Y).
>
> The Shape and the dimensions should not be *separate*;
> they should be in a *single* data structure.
>
> One of the reasons for this is that it is not the case
> that all shapes have the same number of dimensions.
>
> To use Haskell syntax here, imagine
>
> data Shape
>    = Square Float               -- side
>    | Rectangle Float Float      -- sides
>    | Rhombus Float Float Float  -- sides, acute angle
>    | Triangle Float Float Float -- sides
>    | Triangle2 Float Float      -- base altitude
>    | Circle Float               -- radius
>    | Ellipse Float Float        -- semi-major & semi-minor
>    | Point
>
> We see that the number of dimensions naturally varies from
> 0 to 3.  Now this _could_ be represented as a Shape atom
> and a Dimensions list, but done that way there's no
> *connection* to make sure the two agree.
>
> It's far better to use
>
>   {square,Side}
> | {rectangle,Width,Height}
> | {rhombus,Side1,Side2,Angle}
> | {triangle3,Side1,Side2,Side3}
> | {triangle2,Base,Altitude}
> | {circle,Radius}
> | {ellipse,SemiMajor,SemiMinor}
> | {point}
>
> So it should be
>
> area() ->
>     Figure = figure_from_user(),
>     Area = area(Figure),
>     report_area_to_user(Area).
>
> Anyone else on the list remember
> "Hierarchical Input-Process-Output"?
> Input, processing, and output generally *should* be separated;
> putting area/1 and report_area_to_user/1 into a single operation
> is, um, looking for a tactful phrase, ah got it, suboptimal.
>
> I would expect someone posting from a .nl address
> to be acutely aware of the fact that there are many
> human languages in the world.  (About 6000, as far as
> I know, with many hanging on by their fingernails.)
>
> Reading from the user needs to be localised.
> Writing to the user needs to be localised.
> The processing that happens in between does not.
>
> As noted before, I even reject the idea of prompting
> for the shape and dimensions separately, or if they
> are separate, it would be good to make the separation
> table-driven.  Something like this:
>
> dimensions(square) -> [side];
> ...
> dimensions(circle) -> [radius];
> dimensions(point)  -> [].
>
> figure_from_user() ->
>     Shape = get the shape from the user,
>     Dimension_Names = dimensions(Shape),
>     Dimension_Values = get each dimension from the user(Dimension_Names),
>     list_to_tuple([Shape|Dimension_Values]).
>
> This way 'get each dimension from the user' doesn't know
> ANYTHING about which shapes have what dimensions and a
> new shape can be added very easily.
>>
>> numbers_from_user(XPrompt, YPrompt) ->
>>    X = number_from_user(XPrompt),
>>    Y = number_from_user(YPrompt),
>>    {X, Y}.
>
> Make this
>
> numbers_from_user([]) ->
>     [];
> numbers_from_user([Prompt|Prompts]) ->
>     Number  = number_from_user( Prompt),
>     Numbers = numbers_from_user(Prompts),
>     [Number | Numbers].
>
> Why on earth should this function know that there are
> exactly two dimensions when the number *ought* to vary?
>>
>> to_number(Str) ->
>>    try_number([fun list_to_float/1, fun list_to_integer/1], Str).
>>
>> try_number([Method|Rest], Arg) ->
>>    try
>>        Method(Arg)
>>    catch
>>        error:badarg -> try_number(Rest, Arg)
>>    end;
>> try_number([], _Arg) ->
>>    error(bad_number).
>
> This is overkill.  Here there are two and only two functions
> you ever intend to call.

The alternative form that occurs to me, which calls the two functions
directly is:

to_number(Str) ->
    try
        list_to_float(Str)
    catch
        error:badarg ->
            try
                list_to_integer(Str)
            catch
                error:badarg ->
                    error(bad_number)
            end
    end.

Not only is this hard to read, it's longer in line and character count
than my version!

More importantly, generalizing to_number/1 here as a series of
attempts clearly spells my intention.

As a nice side effect, if I wanted to add try_octal (as to follow your
own argument, a number from a user is not inherently two possible
things) it's a trivial and coherent change.

Garrett



More information about the erlang-questions mailing list