How to Get SPEC-OF or Parameters Of Functions

In Rebol2 and Red you can ask for a function's spec, to see how it was written:

rebol2>> print mold spec-of :append
[
    {Appends a value to the tail of a series and returns the series head.}
    series [series! port!]
    value
    /only "Appends a block value as a block"
]

If you want you can just ask for the words, and find out which one is a refinement:

rebol2>> words-of :append
== [series value /only]

And if you want the types you can ask for those too...although they expand out to blocks containing many types in Rebol2... e.g. ANY-TYPE! is expanded to all types, SERIES! to all series types, etc.

rebol2>> print mold types-of :append

[[string! binary! file! email! url! tag! issue! image! block! paren! path! set-path! lit-path! hash! list! port!] [none! logic! integer! decimal! money! char! pair! tuple! time! date! string! binary! file! email! url! tag! issue! bitset! image! block! paren! path! set-path! lit-path! datatype! word! set-word! get-word! lit-word! refinement! native! action! routine! op! function! object! port! event! struct! library! hash! list! symbol!] [none! logic!]]

(Red does not offer TYPES-OF, so you have to parse this information out of SPEC-OF).

No SPEC-OF in Ren-C, and WORDS OF has no Parameter Types

If you ask for the words, there's no decorations. You don't know if the parameters are refinements or quoted, etc.

ren-c>> words of append/
== [series value part dup line]

How do you access the function spec to do "metaprogramming" in Ren-C?

1 Like

While Rebol2 and Red have reflection properties that are better than nothing, they aren't that much better. Everyone had to reparse the spec language, and it was clunky and error-prone. The TYPESET! debacle was particularly bad.

On top of this, parsing to rediscover information the system already knows is not a performant idea.

But As Is Often The Case, Ren-C Has Good News! :partying_face:

We introduce a fundamental PARAMETER! type, which gives you powerful handling of everything you could want to know about a function's interface... and helps build derived functions and APPLY-like operators programmatically.

If you know the name of a function's parameter, just pick it by name using TUPLE! access:

>> append.series
== &[parameter! [<opt-out> any-series? port! map! object! module! bitset!]]

>> append.value
== &[parameter! [<opt-out> element? splice!]]

>> append.dup
== &[parameter! :[any-number? pair!]]

If a parameter is required and not a refinement, you can use indexes as well:

>> append.1
== &[parameter! [<opt-out> any-series? port! map! object! module! bitset!]]

>> append.2
== &[parameter! [<opt-out> element? splice!]]

PARAMETER! offers a number of fields, that are null if not present

>> append.series.text
== "Any position (modified)"

>> append.series.spec
== [<opt-out> any-series? port! map! object! module! bitset!]

You Can Reflect The Return Spec

You can ask about a function's return value using RETURN OF, which gives you a parameter describing the types of the return value:

>> return of append/
== &[parameter! [any-series? port! map! object! module! bitset!]]

But don't confuse return of append/ with append.return... the former works on all functions and the latter would only work on a function which had a parameter called "return" on its public interface.

Here's an example demonstrating the difference:

shipping: lambda [
    "Either order a product or return it, give status string"
    []: [text!]
    product [text!]
    :return "process shipping as a return, give order ID"
        [integer!]
    {id}   ; new locals specification via FENCE! -> CONSTRUCT dialect
] bind construct [
    next-id: 1  ; static via BIND, improvements are being considered
] [
    either id: return [
        print ["Returning" product "ID:" id]
    ] [
        id: next-id
        next-id: me + 1
        unspaced ["Ordering" product "ID:" id]
    ]
 ]

>> shipping.return
== &[parameter! :[integer!]]

>> shipping.return.text
== "process shipping as a return, give order ID"

>> return of shipping/
== &[parameter! [text!]]

>> (return of shipping/).text
== "Either order a product or return it, give status string"

>> return.text of shipping/  ; alternate (proposed TUPLE! OF behavior)
== "Either order a product or return it, give status string"

(Notice also that the function's "description" which appears before the lambda's return typespec actually winds up being the text of the (return of) parameter. There's not really any other good place to put it, so this makes a lot of sense...even though the text in the spec appears before the thing it describes instead of after it.)

Naming a function parameter RETURN on a whim is clearly not something you'd want to do. But if someone passed you a function called return that was intended to be used as a return that could be useful. And of course, the ability to create locals called RETURN is paramount if you want to create your own LAMBDA-derived generators which implement RETURN-like concepts other ways.

PARAMETER! Also Exposes Other Properties

If something is "a refinement" it currently reports that the PARAMETER! is "optional":

>> shipping.return.optional
== \~okay~\  ; antiform (keyword!)

I'm still hammering out the exact names and classes of these properties, so I'll just mention that one for now.

PARAMETER! Does Fast Checks In Typecheck-Like Places

You can use a PARAMETER! in places where you can do typechecking-like things, and it's actually optimized to be faster than specifying a list of types in a BLOCK!.

>> append.series
== &[parameter! [<opt-out> any-series? port! map! object! module! bitset!]]

>> match append.series 10
== \~null~\  ; antiform (keyword!)

>> match append.series [a b c]
== [a b c]

So if you have a test like typecheck [integer! block!] that you're doing really often, you actually get a speedup by saying:

int-or-block: make parameter! [integer! block!]
for-each item list [
    if typecheck int-or-block item [...]
]

In this way, PARAMETER! is the closest thing to a TYPESET! Ren-C has.

The Real Power: FRAME! and PARAMETER!

You can enumerate parameters on frames, so just remove the antiform status from a function and you've got an object-like structure with parameters in it.

If you use the DECORATE function you can ask to decorate a parameter to match what it would look like in a function spec (so putting the leading colon on, etc.)

>> for-each [key param] unanti append/ [
       print [mold decorate param key, opt mold opt param.spec]
   ]

series [<opt-out> any-series? port! map! object! module! bitset!]
value [<opt-out> element? splice!]
:part [any-number? any-series? pair!]
:dup [any-number? pair!]
:line

(Notice the safety there: param.spec can be null... e.g. :LINE is a refinement with no arguments... in which case you can't mold null. So you use OPT to make VOID, which then makes MOLD return NULL... and PRINT doesn't know if that's on purpose, so you have to OPT that again and it vaporizes! All of this guides you toward writing code that doesn't just do random things.)

And you can interconvert between frames and actions, creating specializations or alterations that tweak the parameters all you want.

(In practice there's a bit of a devil in the details regarding how easy or hard it is to compose these type spec blocks, but the system can only do what is possible...)

So You See Why SPEC OF is Not In The Box :wrapped_gift:

You have a much better set of tools in your hand.

You really can't know.

From the outside of a function, all we know is the interface it gives. As a brutally simple example: it might be a LAMBDA, in which case we don't want to put a RETURN:.

Arbitrary generators make these functions, so the best thing to tell you is being honest and direct about what we do know--the FRAME! interface--and let you work from there.

I've shown it's easy enough to write a SPEC-OF-like thing for your specific purpose that there's little value in making a version that would be dishonest in some way.

1 Like