Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

Discuss Autohotkey related topics here. Not a place to share code.
Forum rules
Discuss Autohotkey related topics here. Not a place to share code.
sashaatx
Posts: 333
Joined: 27 May 2021, 08:27
Contact:

Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

13 Feb 2024, 02:40

This is addressed towards authors in the community of Class Libs. I believe we should adopt the python-esk clean data standard.
The below example and titles' thesis would not only clean up organization and standardized, it would heavily encourage an abstracted focused formatting.
As in python, a script should be able to run on its own without the required parent script.
Ill use a positive example, then an example of my scripts docs which I try to standardize, but most lib writters I see/fork don't standardize even for Javadoc/js format.

Positive example

Code: Select all

if A_LineFile = A_ScriptFullPath 
    pantryExample()

pantryExample() 
{
    ; Example usage:
    PantryAPI.PantryID := "YOUR_PANTRY_ID" ; Set your Pantry ID
    PantryAPI.BasketName := "YOUR_BASKET_NAME" ; Set your Basket Name
    ; https://getpantry.cloud/#
    MsgBox PantryAPI.NewBasket()
    ; continues.....
}

class PantryAPI {
    static PantryID := "" ; Your Pantry ID here
    static BasketName := "" ; Your Basket Name here
    ; Method to update the contents of a basket
    static UpdateBasket(newData) {
    ; continues....
Negative example, and how many classes are written now. Using an old script of my own, not picking on anyone.

Code: Select all

#Include _Jsons.ahk
/*
    @source https://github.com/samfisherirl/Github.ahk-API-for-AHKv2
    @method Github.latest(Username,Repository_Name)

    return {
        downloadURLs: [
            "http://github.com/release.zip",
            "http://github.com/release.rar"
                    ],
        version: "",
        change_notes: "",
        date: "",
        }
        
    @method Github.historicReleases(Username,Repository_Name)
        array of objects => [{
            downloadURL: "",
            version: "",
            change_notes: "",
            date: ""
        }]
    @func this.Download(url,path)
        improves on download(): 
        - if user provides wrong extension, function will apply proper extension
        - allows user to provide directory 
        example:    Providing A_ScriptDir to Download will throw error
                    Providing A_ScriptDir to Github.Download() will supply Download() with release name 
*/
class Github
{
    static source_zip := ""
    static url := false
    static usernamePlusRepo := false
https://github.com/samfisherirl
? /Easy-Auto-GUI-for-AHK-v2 ? /Useful-AHK-v2-Libraries-and-Classes : /Pulovers-Macro-Creator-for-AHKv2 :
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

13 Feb 2024, 12:20

I may be misunderstanding, but I don't agree with this suggestion. I don't intend to be harsh here, just thorough, so if I come off as aggressive at all I do apologize.

Python's if __name__ == '__main__': construct, in my opinion at least, has more to do with making runnable files also includable rather than the other way around of making includable library files also runnable. So I don't necessarily agree that this is a Python-esque standard, but that's neither here nor there. I have more basic problems with the premise.

Small examples to demonstrate particular features of a library should not be divorced from the methods implementing that behavior. The further away the examples are from the method, the less likely someone who needs to see the example will see it at the time they would most benefit from seeing it. Additionally, the further away they are the more likely they are to be ignored when making changes to the method that would break compatibility. Though this is less of a concern if those examples are unit tested somehow.

Larger examples I believe belong in their own separate files rather than mixing concerns within the same file. Practices pioneered in other more established languages dictate more or less that a project should contain one class definition per library file, with that file sharing the name of the class. Not all AHK files should be class-based, but for the ones that are I think it's important to do that. Breaking the convention can make it more difficult to find what you are looking for in large unfamiliar projects, as well as cause confusion about include paths for complicated dependency chains. Granted, we do not have to deal with things like PHP autoloaders which actually require that this convention be followed.

I believe projects really should not be monolithic with separate concerns all together in one file. Whereas in web design we break up document structure, document styling, and dynamic behavior, in AHK we might break up implementation, examples, and unit tests.

Here in Neutron for example, the Load method contains a brief description, long description, minimal example, and other metadata following the documentation format to allow Intellisense under thqby's VSCode AHKv2 extension.

Code: Select all

class NeutronWindow {

	/**
	 * Loads an HTML file by name (not path).
	 * 
	 * When running the script uncompiled, looks for the file in the local
	 * directory. When running the script compiled, looks for the file in the
	 * EXE's RCDATA. Files included in your compiled EXE by FileInstall are
	 * stored in RCDATA whether they get extracted or not. An easy way to get
	 * your Neutron resources into a compiled script, then, is to put
	 * FileInstall commands for them at the start or end of the AutoExecute
	 * section, wrapped in `if False {}`. For example:
	 * 
	 * ```ahk2
	 * ; AutoExecute Section
	 * neutron := NeutronWindow().Load("index.html").Show()
	 * 
	 * if False {
	 *     FileInstall "index.html", "*"
	 *     FileInstall "index.css", "*"
	 * }
	 * return
	 * ```
	 * 
	 * @param fileName The name of the HTML file to load into the Neutron window.
	 *                 Make sure to give just the file name, not the full path.
	 * 
	 * @return {NeutronWindow} The instance, for chaining
	 */
	Load(fileName) {
With this documentation, hovering the method wherever it is called produces a tooltip containing the example with appropriate syntax highlighting, allowing someone who is using the library to benefit from the minimal example without having to search for it specifically.

Image

Then in addition to this small example directly inside the in-code documentation block comment, several distinct examples are available in the examples folder for anyone who needs a full example. That there is more than one example is notable I think, as there is not an obvious solution to including multiple full examples under the if A_LineFile = A_ScriptFullPath. I suppose it would require you to build in some kind of example-launching UI, which is extra boilerplate code not really related to the underlying library.

I see some potential issues with your positive example, though I think they can be addressed.

- Under Python it is standard to name the entrypoint function used by this construct main, but AutoHotkey's lack of namespacing means you cannot have multiple main functions in separate files so it is not a suitable name to use in a library. You also cannot create a function sharing the name of the class, so assuming you name both the entrypoint and the class the same as the file name, the entrypoint will require a suffix which should be standard if your goal is to provide uniformity across libraries. Your choice of pantryExample doesn't fit, because it diverges from the name PantryAPI, so anyone including the file who would want to invoke the entrypoint for whatever reason has to guess or look it up, and anyone reading that code would have to guess which include file it came from. If I could suggest instead, PantryAPI_Main might be a more suitable name/naming scheme for this entrypoint function.

- Your example is not appropriate for code that may ever be compiled. Example snippets that are in some kind of documenting comment gets stripped out of the resulting exe by ahk2exe, examples that live in a separate file never get loaded into the runtime to begin with, but example code behind a guard like the one you're using makes its way all the way to the resulting exe file and (as written) actually becomes active code because A_LineFile and A_ScriptFullPath will now both be set to the same exe path.

This can be addressed through at least two options. First is to adjust your guard to check for the compiled status like if A_LineFile = A_ScriptFullPath && !A_IsCompiled. This will prevent the code from activating when the script is compiled, but the code for the example still lives rent-free as dead code in the resulting exe. So for best results, a second option is to use Ahk2Exe compiler directives so that the compiler will know to remove that code from the exe. With that in place, checking for !A_IsCompiled is not necessary because the check itself is no longer there when that condition is true.

Code: Select all

;@Ahk2Exe-IgnoreBegin
if A_LineFile = A_ScriptFullPath 
    pantryExample()

pantryExample() 
{
    ...
}
;@Ahk2Exe-IgnoreEnd

class PantryAPI {
    ...

I do think there could be a place for if A_LineFile = A_ScriptFullPath guarded sections in library files, but maybe something more akin to how in v1 some people would write defensive classes that included __Get/__Set meta-functions to throw exceptions whenever a class was accessed in an unsupported way (like MsgBox % class.tyop would throw because tyop did not exist and they instead meant typo). Here, rather than throwing if the class was accessed inappropriately, it would throw if the file was executed inappropriately. Because of how v2 processes static classes, you could even stick it into the class definition to keep top-level code out of the library file:

Code: Select all

class PantryAPI {
    static __New() {
        if A_LineFile = A_ScriptFullPath && !A_IsCompiled
            throw Error("This file should not be executed directly")
    }
}
Because the check code is just two lines I don't think having it stay in the compiled script as dead code is a huge deal either way, but if that is ever a concern the ahk2exe compiler directive could be applied here as well.

Wishing you the best,
geek
User avatar
Chunjee
Posts: 1422
Joined: 18 Apr 2014, 19:05
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

13 Feb 2024, 13:48

sashaatx wrote:
13 Feb 2024, 02:40
As in python, a script should be able to run on its own without the required parent script.
Never seen this in Javascript. ahk libraries should be used with #include; never running directly

geek wrote:
13 Feb 2024, 12:20
I believe projects really should not be monolithic with separate concerns all together in one file. Whereas in web design we break up document structure, document styling, and dynamic behavior, in AHK we might break up implementation, examples, and unit tests.
This sparks joy
sashaatx
Posts: 333
Joined: 27 May 2021, 08:27
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

13 Feb 2024, 15:12

geek wrote:
13 Feb 2024, 12:20
I may be misunderstanding, but I don't agree with this suggestion. I don't intend to be harsh here, just thorough, so if I come off as aggressive at all I do apologize.

Python's if __name__ == '__main__': construct, in my opinion at least, has more to do with making runnable files also includable rather than the other way around of making includable library files also runnable. So I don't necessarily agree that this is a Python-esque standard, but that's neither here nor there. I have more basic problems with the premise.

Small examples to demonstrate particular features of a library should not be divorced from the methods implementing that behavior. The further away the examples are from the method, the less likely someone who needs to see the example will see it at the time they would most benefit from seeing it. Additionally, the further away they are the more likely they are to be ignored when making changes to the method that would break compatibility. Though this is less of a concern if those examples are unit tested somehow.

Larger examples I believe belong in their own separate files rather than mixing concerns within the same file. Practices pioneered in other more established languages dictate more or less that a project should contain one class definition per library file, with that file sharing the name of the class. Not all AHK files should be class-based, but for the ones that are I think it's important to do that. Breaking the convention can make it more difficult to find what you are looking for in large unfamiliar projects, as well as cause confusion about include paths for complicated dependency chains. Granted, we do not have to deal with things like PHP autoloaders which actually require that this convention be followed.

I believe projects really should not be monolithic with separate concerns all together in one file. Whereas in web design we break up document structure, document styling, and dynamic behavior, in AHK we might break up implementation, examples, and unit tests.

Here in Neutron for example, the Load method contains a brief description, long description, minimal example, and other metadata following the documentation format to allow Intellisense under thqby's VSCode AHKv2 extension.

Code: Select all

class NeutronWindow {

	/**
	 * Loads an HTML file by name (not path).
	 * 
	 * When running the script uncompiled, looks for the file in the local
	 * directory. When running the script compiled, looks for the file in the
	 * EXE's RCDATA. Files included in your compiled EXE by FileInstall are
	 * stored in RCDATA whether they get extracted or not. An easy way to get
	 * your Neutron resources into a compiled script, then, is to put
	 * FileInstall commands for them at the start or end of the AutoExecute
	 * section, wrapped in `if False {}`. For example:
	 * 
	 * ```ahk2
	 * ; AutoExecute Section
	 * neutron := NeutronWindow().Load("index.html").Show()
	 * 
	 * if False {
	 *     FileInstall "index.html", "*"
	 *     FileInstall "index.css", "*"
	 * }
	 * return
	 * ```
	 * 
	 * @param fileName The name of the HTML file to load into the Neutron window.
	 *                 Make sure to give just the file name, not the full path.
	 * 
	 * @return {NeutronWindow} The instance, for chaining
	 */
	Load(fileName) {
With this documentation, hovering the method wherever it is called produces a tooltip containing the example with appropriate syntax highlighting, allowing someone who is using the library to benefit from the minimal example without having to search for it specifically.

Image

Then in addition to this small example directly inside the in-code documentation block comment, several distinct examples are available in the examples folder for anyone who needs a full example. That there is more than one example is notable I think, as there is not an obvious solution to including multiple full examples under the if A_LineFile = A_ScriptFullPath. I suppose it would require you to build in some kind of example-launching UI, which is extra boilerplate code not really related to the underlying library.

I see some potential issues with your positive example, though I think they can be addressed.

- Under Python it is standard to name the entrypoint function used by this construct main, but AutoHotkey's lack of namespacing means you cannot have multiple main functions in separate files so it is not a suitable name to use in a library. You also cannot create a function sharing the name of the class, so assuming you name both the entrypoint and the class the same as the file name, the entrypoint will require a suffix which should be standard if your goal is to provide uniformity across libraries. Your choice of pantryExample doesn't fit, because it diverges from the name PantryAPI, so anyone including the file who would want to invoke the entrypoint for whatever reason has to guess or look it up, and anyone reading that code would have to guess which include file it came from. If I could suggest instead, PantryAPI_Main might be a more suitable name/naming scheme for this entrypoint function.

- Your example is not appropriate for code that may ever be compiled. Example snippets that are in some kind of documenting comment gets stripped out of the resulting exe by ahk2exe, examples that live in a separate file never get loaded into the runtime to begin with, but example code behind a guard like the one you're using makes its way all the way to the resulting exe file and (as written) actually becomes active code because A_LineFile and A_ScriptFullPath will now both be set to the same exe path.

This can be addressed through at least two options. First is to adjust your guard to check for the compiled status like if A_LineFile = A_ScriptFullPath && !A_IsCompiled. This will prevent the code from activating when the script is compiled, but the code for the example still lives rent-free as dead code in the resulting exe. So for best results, a second option is to use Ahk2Exe compiler directives so that the compiler will know to remove that code from the exe. With that in place, checking for !A_IsCompiled is not necessary because the check itself is no longer there when that condition is true.

Code: Select all

;@Ahk2Exe-IgnoreBegin
if A_LineFile = A_ScriptFullPath 
    pantryExample()

pantryExample() 
{
    ...
}
;@Ahk2Exe-IgnoreEnd

class PantryAPI {
    ...

I do think there could be a place for if A_LineFile = A_ScriptFullPath guarded sections in library files, but maybe something more akin to how in v1 some people would write defensive classes that included __Get/__Set meta-functions to throw exceptions whenever a class was accessed in an unsupported way (like MsgBox % class.tyop would throw because tyop did not exist and they instead meant typo). Here, rather than throwing if the class was accessed inappropriately, it would throw if the file was executed inappropriately. Because of how v2 processes static classes, you could even stick it into the class definition to keep top-level code out of the library file:

Code: Select all

class PantryAPI {
    static __New() {
        if A_LineFile = A_ScriptFullPath && !A_IsCompiled
            throw Error("This file should not be executed directly")
    }
}
Because the check code is just two lines I don't think having it stay in the compiled script as dead code is a huge deal either way, but if that is ever a concern the ahk2exe compiler directive could be applied here as well.

Wishing you the best,
geek
I think everything you said is fair, what I likely mixed up was my personal approach to writing modules vs the reason why python at its core provides this capability. As in the title, my goals and opinions rarely reflect those of the community and I always appreciate insight on some of those more experienced.
I would say more conservatively than last night- unless Im writing a util file or lib, if I include Json.ahk, I want that json.ahk to run out of the box and standalone if needed. More importantly, if I return or look to include in the future this file, I want to be able to debug and breakpoint that file before I import and having the example in place cuts out the need for the documentation example and gives the additive functionality of running out of the box.


Lastly, JSDoc may be a more apt descriptor. https://jsdoc.app/
@function @author etc. I think I pulled cJson providing those a week ago.
https://github.com/samfisherirl
? /Easy-Auto-GUI-for-AHK-v2 ? /Useful-AHK-v2-Libraries-and-Classes : /Pulovers-Macro-Creator-for-AHKv2 :
User avatar
Chunjee
Posts: 1422
Joined: 18 Apr 2014, 19:05
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

13 Feb 2024, 20:10

https://ahkpm.dev/ may be of interest to you.

I would be a bit more onboard if they quadrupled compatibility by using the filename 'package.json' instead (npm docs, yarn docs, pnpm docs)

this format I believe is better for documenting authors, contributors, dependencies, etc. But does not offer any in editor usage tips

the python comparison is named requirements.txt I think
sashaatx
Posts: 333
Joined: 27 May 2021, 08:27
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

14 Feb 2024, 15:01

geek wrote:
13 Feb 2024, 12:20
[Mod edit: Reduced giant, unspecific quote. Please use quotes more selectively.]

hi @geek quick question as a uninformed user of interpreted languages I understand a bit about static methods, try to keep my class methods static where possible to reduce memory overhead,

static __New()

intuitively this seems strange and maybe contrary in their purpose. what is the significance or use of making instantiation a static method?

again, I dont know, any intuition I have is incorrect and not a critique, just an insight into my confusion.
https://github.com/samfisherirl
? /Easy-Auto-GUI-for-AHK-v2 ? /Useful-AHK-v2-Libraries-and-Classes : /Pulovers-Macro-Creator-for-AHKv2 :
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: Opinion: For AHKv2, We should move away from writing Examples into inline comment docs, adopt if __main__

15 Feb 2024, 08:18

In AutoHotkey v2, all class definitions create multiple objects that explicitly are not instances. Primarily, a class definition creates a class object and a prototype object. The class object is the one stored in the global read-only variable by the same name of the class. The class object is callable, and acts as a factory for instances. The callable factory creates a new object that becomes an instance, whose base is set to the prototype object and then the factory is in charge of invoking the __Init and __New methods to allow the instance to be constructed.

When you define a method or property as static, it gets applied to the class object rather than the prototype object that instances are made from. Just like how __New on the prototype object gets invoked when creating instances, __New on the class object gets invoked at load-time when AHK creates the class object. To be precise, __New on the class object will be called either when auto-execution goes past the class definition, or when the class name is referenced before auto-execution goes past the class definition.

Code: Select all

#Requires AutoHotkey v2.0
/*
This definition is roughly equivalent to the below un-commented prototype based code,
though the class syntax does have some additional behavior for convenience, like
hoisting (accessing the class above the point where it is defined).

class Test {
    static greeting := "Hello"
    static __New() => this.greeting := "Hey"

    Greet(name) => MsgBox(Test.greeting " " name)
}

instance := Test()
instance.Greet(A_UserName)
*/

global Test := {
    greeting: "Hello",
    __New: (this) => this.greeting := "Hey",
    prototype: {
        Greet: (this, name) => MsgBox(Test.greeting " " name)
    },
    Call: (this) => (
        inst := {base: this.prototype},
        inst.__Init(),
        HasMethod(inst, "__New") ? inst.__New() : "",
        inst
    )
}
Test.__New()

instance := Test()
instance.Greet(A_UserName)
As for how this can be useful, well, that's up to the creativity of the developer. I use a static constructor in cJson to load the machine code up-front rather than waiting for someone to call to the cJson library first. In v1, I had a call to the machine code loader as the first line of all static methods, just to make sure that it gets loaded before anything else happens. Other v1 libraries had explicit initialization functions you had to call before using the library, like tic's gdip library where you'd call gdip_startup.

The static constructor is pretty similar to how in AHKv1 it was a somewhat common tactic to put library initialization code as a dummy static variable in a function definition, so that the library instantiation would occur before the autoexecute section. But better, because the order in which initialization occurs is now actually defined usefully.

Code: Select all

#Requires AutoHotkey v1.0
initMyLibrary() {
    static _ := initMyLibrary() ; Make this function get called by AHK before auto-execution begins
    ; ... do whatever it is that is needed to initialize the library
}
Tangentially, do you think it would be a good idea for me to work on writing an interactive wiki article about v2 class anatomy? With those little embedded editable examples you can click to run and get the output from. Like the machine code guide.

Return to “General Discussion”

Who is online

Users browsing this forum: No registered users and 54 guests