Expand/Shrink

Procedures

These perform some computation and may have a list of parameters, e.g.
    procedure plot(integer x, y, char='*')
        position(x, y)
        puts(1, char)
    end procedure
Most procedure names should normally begin with a verb, to make it clearer what it is actually doing, though often nouns and adjectives are fine too.
A copy of the value of each argument is passed in. The formal parameter variables may be modified inside the procedure but this does not affect their values in the calling code - for that you need a function with an explicit return and [re-]assignment.

Repeated consecutive parameter types may be omitted, in the above the compiler assumes y and char are also integer, though it would not hurt any to explicitly specify integer three times.

The end keyword is also used in function, type, enum, struct, and class defintions, as well as if, switch, for, while, and try statements.
Performance Note:

The interpreter does not actually copy sequences or floating-point numbers unless it becomes necessary. For example,
     y = {1,2,3,4,5,6,7,8.5,"ABC"}
     x = y
The statement x = y does not actually cause a new copy of y to be created. Both x and y will simply "point" to the same sequence. If we later perform x[3] = 9, then a separate sequence will be created for x in memory (although there will still be just one shared copy of 8.5 and "ABC") and y is not altered. The same thing applies to "copies" of arguments passed in to subroutines.

If there are no parameters, the parentheses are still required, on both the procedure declaration and any calls to it, e.g.
    procedure empty()
    end procedure
    ...
    empty()
While Phix allows implicit forward calls, you can also explicitly declare a forward routine definition, for example:
    [global] forward procedure plot(integer x, y, char='*')
The definition line should match the actual (later) definition exactly, in both the names and defaults of any parameters, but the body and end procedure(/function) should not be present. It can certainly help the compiler to specify a forward global routine, otherwise it assumes local and has to make hasty and potentially error-prone corrections when it finds the actual definition is in fact global, and/or issue compilation errors immediately as opposed to some time after the fact, such as passing/attempting to assign a string to an integer parameter, or excess or missing non-optional arguments.

Optional parameters can be declared simply by defining a default value, as shown above for char (= and := are treated identically in routine declarations). In many languages variable-length parameter lists are impossible. In C, you must set up strange mechanisms that are complex enough that the average programmer cannot do it without consulting a manual or a local guru. In Phix the only rule is that all non-optional parameters must be grouped together on the left, and obviously any defaults must be legal values for the specified type.

Builtins with optional parameters include allocate, find, match, join, split, trim, round, and printf.

Alternatively, optional parameters can be emulated by specifying a single sequence parameter, which can be populated by anything from a zero-length sequence up to the maximum that the routine can handle. This approach is used, amongst others, by printf, call_proc, call_func, define_c_proc, define_c_func, c_proc, c_func, free, min, max, smallest, decode_flags, and join_path, as well as all the explicit sequence operators.

Named parameters can be used in any order desired using <name>:=<value> syntax. Quite possibly the ultimate example of named parameters is the timedelta function:
    atom secs
    secs = timedelta(0,0,7,30,0,0,0)
    secs = timedelta(hours:=7, minutes:=30)
Both calls achieve exactly the same thing, but obviously there is a world of difference in code clarity.

You can supply the first few non-optional parameters as normal, and then start using named parameters, or you can use the names of all of them. Obviously you must supply all the non-optional parameters, somewhere, but once a name has been specified then all subsequent parameters must also be named.

Builtins for which I explicitly recommend the use of named parameters (especially for any uncommon or unusual uses) include clear_directory, copy_file, move_file, remove_directory, rename_file, date, join_path, prime_factors, split, split_any, and split_path.

Another example can be found in Edix:
    function saveFile(bool warnBackup=true)
Normal calls, which display a warning if for any reason the backup could not be made, are invoked as saveFile(), whereas saveFile(warnBackup:=false) is used when performing saveas or similar, in which case it still tries to make a backup but does not display any warning should it fail. The named parameter makes the slightly unusual invocation much clearer and self-documenting.

Alternatively, a local variable can be declared (eg bool warnBackup = false) and used as the (positional based) parameter, which will obviously work just as well, both in terms of program functionality and the improved code clarity. Although it requires no specific compiler support, and makes no check that such a parameter actually exists, I might sometimes also describe that mechanism as named parameters.

NB: On call statements, ":=" signifies the use of named parameters, whereas "=" has an entirely different meaning.
It is perfectly legitimate, assuming you have local variables x and y, to write plot(x:=x,y:=y), which would be equivalent to the simpler plot(x,y), whereas in the same circumstances the statement plot(x=x,y=y) would, barring unassigned errors, always be equivalent to plot(true,true), aka plot(1,1).

Although somewhat obscure, it may be worth understanding the derivation of named parameter syntax: from proc(proc:name=value) via proc(:name=value) to the recommended proc(name:=value), with all three forms being perfectly legal code (at least on desktop/Phix, but only the third one under pwa/p2js), what looks like a normal explict assignment operator is in fact a special "assign to the parameter namespace of the routine being called" operator.

Un-named parameters are also permitted (added in version 0.6.8). In many cases, especially callbacks, a routine must accept a fixed set of parameters, even if it does not use them. This can cause annoying "not used" warnings. Instead of
    function mainHandler(integer id, integer msg, atom wParam, object lParam)
        if id or wParam or object(lParam) then end if   -- suppress warnings
(in which the "if then if" generates no code) you can instead declare it as
    function mainHandler(integer /*id*/, integer msg, atom /*wParam*/, object /*lParam*/)
        -- (and obviously all comments, including this one, are optional)
Note that typechecking of un-named parameters still occurs, which can lead to interesting run-time error messages, eg
    procedure test(integer a, integer /*b*/, integer c)
--  procedure test(integer a, /*b*/, c) -- (also legal)
        ?{a,c}
    end procedure
--  test(1,"error",2)   -- (causes a perfectly sensible compile-time error)
    call_proc(routine_id("test"),{1,"error",3})
triggers something like
C:\test\test.exw:1 in procedure test()
type check failure, ???(symtab[472][S_name]=0) is "error"
    a = 1
    c = 3
... called from test.exw:6

--> see C:\Program Files (x86)\Phix\ex.err
Press Enter...
- at least it indicates the offending source code line, and it should be fairly obvious the problem is with "/*b*/", for want of a better name.
Of course no such errors would happen for parameters declared as type object.
You should also be aware that the absence of "/*b*/" in the ex.err file, as deliberately shown above, may sometimes make debugging somewhat more difficult.
One example of the latter that immediately springs to mind is pEmit2.e/blurph() [normally I find it dirt-simple to give things a clear and intuitive name but that one has defeated me for over a decade and I defy anyone to do any better, although now I think of it bleugh() comes close], where I added a lineno parameter purely as a debugging aid. Admittedly I now fiddle with it to make it an even more effective debugging aid, but the implication still stands that making it unnamed would have rendered it ("/*lineno*/") utterly useless.

Procedures, just like fuctions which are explained next, can also be nested.