PreviousUpNext

5.4.9  Mythryl Functions: Defaultable Keyword Parameters

Mythryl draws its power from a few major design features which work together cleanly rather than many little features hacked together in ad hoc fashion. Consequently, Mythryl often lack feature explicitly implemented by other languages, yet proves capable to achieving much the same results by sometimes unexpected application of more general mechanisms.

Function arguments with defaultable keyword parameters provide a case in point. Sometimes a top-level api function takes a large number of potential options, but in practice almost all of them have a characteristic value which they take in the overwhelming majority of practical cases. File-open commands, for example, may typically need just a filename, but have many other options occasionally useful.

Mythryl has no explicit function keyword argument defaulting mechanism. In fact, it has no function keyword arguments at all as such, we just pass anonymous records to functions when we want that effect.

Here is one way to achieve that effect in Mythryl.

We first define the record to be received by the function in question, then a sumtype with one constructor for each defaultable keyword argument, and finally a helper function which translates an argument list of constructor-value pairs into the desired argument record.

The external caller provides a list of just the fields of interest as constructor-value pairs; the rest get default values during the translation; the function actually doing the work sees just the expected record argument.

Here’s the code. open_file is the external interface, hidden_real_open_file_fn is the function doing the real work (here it just prints out its arguments), and diget_keyword_list is the argument list translator:

    #!/usr/bin/mythryl

    Function_Keyword_Record
        =
        {    filename:               String,
             obscure_float_option:   Float,
             obscure_int_option:     Int,
             obscure_string_option:  String
        };

    Function_Keywords_Sumtype
        = FILENAME(              String )
        | OBSCURE_FLOAT_OPTION(  Float  )
        | OBSCURE_INT_OPTION(    Int    )
        | OBSCURE_STRING_OPTION( String )
        ;

    fun digest_keyword_list  keyword_list
        =
        {   filename              =  REF "default filename";   # Or whatever default value you like.
            obscure_float_option  =  REF 0.0;                  # "                                ".
            obscure_int_option    =  REF   0;                  # "                                ".
            obscure_string_option =  REF "";                   # "                                ".

            process_keywords  keyword_list
            where
                fun process_keywords []
                        =>
                        { filename              => *filename,
                          obscure_float_option  => *obscure_float_option,
                          obscure_int_option    => *obscure_int_option,
                          obscure_string_option => *obscure_string_option
                        };

                    process_keywords ((FILENAME string) ! rest)
                        =>
                        {   filename := string;
                            process_keywords rest;
                        };

                    process_keywords ((OBSCURE_FLOAT_OPTION f) ! rest)
                        =>
                        {   obscure_float_option := f;
                            process_keywords rest;
                        };

                    process_keywords ((OBSCURE_INT_OPTION i) ! rest)
                        =>
                        {   obscure_int_option := i;
                            process_keywords rest;
                        };

                    process_keywords ((OBSCURE_STRING_OPTION s) ! rest)
                        =>
                        {   obscure_string_option := s;
                            process_keywords rest;
                        };

                end;
            end;
        };

    fun hidden_real_open_file_fn  (r: Function_Keyword_Record)
        =
        {    # In a real application this is where
             # we would open the requested file.
             # For demo purposes, we just print
             # out the values in our argument record:
             #
             printf "real_fun:  r.filename              = %s\n" r.filename;
             printf "real_fun:  r.obscure_float_option  = %f\n" r.obscure_float_option;
             printf "real_fun:  r.obscure_int_option    = %d\n" r.obscure_int_option;
             printf "real_fun:  r.obscure_string_option = %s\n" r.obscure_string_option;
             printf "\n";
        };


    fun open_file  keyword_list
        =
        hidden_real_open_file_fn (digest_keyword_list  keyword_list);


    open_file [ FILENAME "myfile.txt" ];

    open_file [ FILENAME "myfile.txt",
                OBSCURE_STRING_OPTION "obscure"
              ];

    open_file [ FILENAME              "myfile.txt",
                OBSCURE_STRING_OPTION   "obscurer",
                OBSCURE_INT_OPTION         934146,
                OBSCURE_FLOAT_OPTION        251.2
              ];

Running the above yields:

    linux$ ./my-script
    real_fun:  r.filename              = myfile.txt
    real_fun:  r.obscure_float_option  = 0.000000
    real_fun:  r.obscure_int_option    = 0
    real_fun:  r.obscure_string_option = 

    real_fun:  r.filename              = myfile.txt
    real_fun:  r.obscure_float_option  = 0.000000
    real_fun:  r.obscure_int_option    = 0
    real_fun:  r.obscure_string_option = obscure

    real_fun:  r.filename              = myfile.txt
    real_fun:  r.obscure_float_option  = 251.200000
    real_fun:  r.obscure_int_option    = 934146
    real_fun:  r.obscure_string_option = obscurer

    linux$

The syntax to define such functions is clumsier than would be the case if the Mythryl compiler had a special hack to support this, but this is not a significant problem in practice since such functions are used relatively infrequently and are typically large enough that the extra overhead is not a serious concern.

The syntax to invoke such functions is quite concise and clear.

If you do not like the upper-case keywords SHOUTING AT YOU, it is a trivial matter to write:

    filename              = FILENAME;
    obscure_float_option  = OBSCURE_FLOAT_OPTION;
    obscure_int_option    = OBSCURE_INT_OPTION;
    obscure_string_option = OBSCURE_STRING_OPTION;

Thereafter you can instead write upper-case free calls like:

    open_file [ filename "myfile.txt" ];
    open_file [ filename "myfile.txt", obscure_string_option "obscure" ];

These definitions can be made at either the package definition or package client end of things. Whether they are an improvement is a matter of taste.

For a real-life example of this technique in use see src/opt/gtk/src/easy-gtk.pkg.

Notice that the above solution uses side effects, but that they are very benign, affecting reference cells which are only visible within digest_keyword_list and which live only for the duration of one call to it. Even if two parallel threads running on separate cores were to invoke digest_keyword_list simultaneously there would be no risk of race conditions or other destructive interactions.

If you are purist enough to dislike the solution even so, it is easily rewritten to eschew side effects:

    fun digest_keyword_list  keyword_list
        =
        process_keywords  (keyword_list, "default filename", 0.0, 0, "")
        where

            fun process_keywords ([], filename, obscure_float_option, obscure_int_option, obscure_string_option)
                    =>
                    # Done processing argument list, so
                    # construct and return equivalent
                    # argument record:
                    # 
                    { filename, obscure_float_option, obscure_int_option, obscure_string_option };


                process_keywords (((FILENAME s)              ! rest), filename, obscure_float_option, obscure_int_option, obscure_string_option)
                    =>
                    process_keywords (rest, s, obscure_float_option, obscure_int_option, obscure_string_option };


                process_keywords (((OBSCURE_FLOAT_OPTION f)  ! rest), filename, obscure_float_option, obscure_int_option, obscure_string_option)
                    =>
                    process_keywords (rest,  filename, f, obscure_int_option, obscure_string_option);


                process_keywords (((OBSCURE_INT_OPTION i)    ! rest), filename, obscure_float_option, obscure_int_option, obscure_string_option)
                    =>
                    process_keywords (rest, filename, obscure_float_option, i, obscure_string_option);


                process_keywords (((OBSCURE_STRING_OPTION s) ! rest), filename, obscure_float_option, obscure_int_option, obscure_string_option)
                    =>
                    process_keywords (rest,  filename, obscure_float_option, obscure_int_option, s);
            end;
        end;

This is in most respects a more elegant solution. The problem is that in a production application of the technique there might well be a hundred or more obscure options, resulting in very long process_keywords argument lists. The resulting code would be both slower and harder to read.

Bottom line: Sometimes the “impure” solution is the best one. Engineering is no place for dogmatists.

(This is also an example of a situation in which it would be nice to have a record update syntax something like my_record where field => value which made a copy of a record with one field changed. This is a frequently requested construct, and some variant of it is likely to get implemented one of these days. Doing so would be a nice undergraduate class project and a welcome contribution.)


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext