TypL icon indicating copy to clipboard operation
TypL copied to clipboard

Documentation functionality

Open kee-oth opened this issue 2 years ago • 6 comments

I believe TypL could benefit from implementing documentation functionality.

Types are useful for documentation but they're only a partial strategy. If a developer were to use TypL for a project and also wanted documentation they'd likely reach for JSDoc and omit type information. JSDoc is widespread but it's an old, inactive project with clunky, generally disliked syntax (I only have anecdotal evidence for this claim).

I propose TypL could enter this field to 1) give developers a modern documentation system and 2) enhance TypL itself with more detailed runtime error descriptions. This would both bring more attention to TypL itself and increase the value of and use cases for TypL.

Nice IDE integration for JSDoc-like tooltips would be a big bonus too.

Usage example with rough API:

function getCommaSeparatedNumberStringPair (aBool = bool, aNumber = number, aString = string) {
  return aBool ? string`${aNumber}, ${aString}` : string`No bueno.` 
}
documentation (getCommaSeparatedNumberStringPair, {
  description: 'This function joins a number and string together to create a new string.',
  parameters: {
    aBool: 'some description of aBool',
    aNumber: 'some description of aNumber',
    aString: 'some description of aString',
  },
  returns: 'aNumber comma separated from aString if aBool is true, otherwise a default message.',
})

getCommaSeparatedNumberStringPair(anArbitraryBool, anArbitraryString)

Resulting runtime error:

TypL Error: 
  Invalid types for getCommaSeparatedNumberStringPair.
  Received Arguments:
     1. VALID: a boolean value
     4. INVALID: a string value
     5. INVALID: no value given
  Function Parameters:
     1. aBool: boolean - some description of aBool
     2. aNumber: number - some description of aNumber
     3. aString: string - some description of aString
  Suggestion:
    Fix Received Argument types for #2 and #3 to match Function Parameters.

"Function Parameters" above is where we see the mixture of TypL's type information and the user-generated documentation. I'm sure we could come up with other benefits of mixing type information and documentation as well.

documentation could be configurable to spit out more or less information. For example: a developer could configure the output to include description and returns, unlike what you see above.

Benefits of a function-based API (as opposed to JSDoc style comments):

  1. Modularity
  2. Composability
  3. Flexibility
  4. Ergonomic
  5. Smaller learning curve, no special syntax
  6. Automatic IDE support for syntax

Basically, it's JS, do what you want to.

Simple example showing how an application could have a common dictionary of terms to be used around the codebase

// commonDictionary.docs.js
export const organizationId = string`the id for an organization`
export const organizations = string`a list of organizations`


// myModule.js
import { organizationId, organizations } from './commonDictionary.docs.js'

const deleteOrganization = (organizationId = string, organizations = array) => {
  return organizations.filter (organization => organization.id !== organizationId)
}
documentation (deleteOrganization, {
  description: 'This function removes an organization',
  parameters: {
    organizationId,
    organizations,
  },
  returns: 'the updated organizations',
})

const updateOrganization = (organizationId = string, organizations = array, updateProperties = object) => {
  return organizations.map (organization => {
    return organization.id === organizationId
      ? { organization, ...updateProperties }
      : organization
  })
}
documentation (updateOrganization, {
  description: 'This function updates an organization',
  parameters: {
    organizationId,
    organizations,
    updateProperties: 'the properties to update an organization with',
  },
  returns: 'the updated organizations',
})

kee-oth avatar Mar 16 '22 04:03 kee-oth

Since TypL already uses `template literals` to embed custom syntax, I am wondering/hoping maybe we could figure out a way to piggyback additional comments directly in the type annotations.

For example:

isChecked = bool`${obj.someFlag} : a comment about this variable`;

// or... 

isChecked = bool`${obj.someFlag}${ "a comment about this variable" }`;

// or... 

isChecked = bool`${obj.someFlag}${{
   description: "a comment about this variable"
}}`

getify avatar Mar 16 '22 14:03 getify

I can't but help notice the following when I see this pattern, and it intrigues me.

Maybe my idea would be better suited in a different project, but I'll leave it here since this inspired me.


I'm curious, if w/ functional comments, if the function signature / body could be added in there as well, where whatever was running the "documentation" function would return the execution function directly, and use the other variables as properties to send to whatever JS documentation utility of choice, as well as help construct some testing templates in w/ the test runner of choice.

Some configuration tool for the "documentation" runner could provide callback handlers for testing output, doc generation, so this stuff could be plugged into other, existing, utilities.

You could configure it to do runtime and / or compile time checking, etc. (it shouldn't even require a compilation step, just to run it).

// Where "def" is the some collective function definer w/ potential lifecycle hooks, etc.

const updateOrganization = def ('updateOrganization', {
  description: 'This function updates an organization',
  parameters: {
    // Note: My examples are bad here, but the goal would be to be able to use
    // "native" types directly along w/ comments... this isn't how it would actually
    // look in code, though
    organizationId: String,
    organizations: Object[],
    updateProperties: 'the properties to update an organization with',
  },
  returns: 'the updated organizations',
  // "signature" could be renamed "body", "exec" or something (the actual function to run)
  signature: (....) => {
    // ... do cool stuff
  }
})

Call it like so

updateOrganization(organizationId, organizations, updateProperties) // ... do cool stuff

jzombie avatar Mar 16 '22 15:03 jzombie

@getify

Pros

  1. Developers don't have to import documentation function
  2. Better for quick documentation
  3. Clearly a TypL enhancement

Cons

  1. I worry that would get a bit unwieldy when used around the app
  2. How would it work with documenting function parameters without default values?
  3. Documentation is fragmented. To get the full idea of the function documentation you'd have a scan a few different places and put it all together in your head.
  4. Limits spinning off documentation as a standalone project

I think having both APIs: function-based and inline would be great. It makes "two ways to do something" but the two different ways are different enough to not cause much confusion I think.

@jzombie

I actually had the same idea! I can see myself enjoying this but I think it's "weird" so it might hurt developer adoption if this was the only way. And I imagine it'd be hard to "strip" the documentation stuff out when minifying a project. I figure it'd be easier to just get rid of documentation calls vs separating it out. But maybe @getify actually knows how difficult that would be.

kee-oth avatar Mar 16 '22 17:03 kee-oth

Another option that would make exporting/importing documention pretty easy. We could curry the documentation function if the function isn't supplied:

// commonDictionary.docs.js
export const organizationId = string`the id for an organization`
export const organizations = string`a list of organizations`
// organizations.docs.js
import { organizationId, organizations } from './commonDictionary.docs.js'

const deleteOrganization = documentation ({
  description: 'This function removes an organization',
  parameters: {
    organizationId,
    organizations,
  },
  returns: 'the updated organizations',
})

const updateOrganization = documentation ({
  description: 'This function updates an organization',
  parameters: {
    organizationId,
    organizations,
    updateProperties: 'the properties to update an organization with',
  },
  returns: 'the updated organizations',
})

export default {
  deleteOrganization,
  updateOrganization,
}
// organizations.js
import organizationDocs from './organizations.docs.js'

const deleteOrganization = (organizationId = string, organizations = array) => {
  return organizations.filter (organization => organization.id !== organizationId)
}
organizationDocs.deleteOrganization(deleteOrganization)


const updateOrganization = (organizationId = string, organizations = array, updateProperties = object) => {
  return organizations.map (organization => {
    return organization.id === organizationId
      ? { organization, ...updateProperties }
      : organization
  })
}
organizationDocs.updateOrganization(updateOrganization)

kee-oth avatar Mar 16 '22 17:03 kee-oth

The above idea also makes sharing definitions really easy:

// commonDictionary.docs.js
export const organizationId = string`the id for an organization`
export const organizations = string`a list of organizations`
// organizations.docs.js
import { organizationId, organizations } from './commonDictionary.docs.js'

const manageOrganizations = documentation ({
  parameters: {
    organizationId,
    organizations,
  },
  returns: 'the updated organization',
})

export default {
  manageOrganizations,
}
// organization.js
import { manageOrganizations } from './organizations.docs.js'

const deleteOrganization = (organizationId = string, organizations = array) => {
  return organizations.filter (organization => organization.id !== organizationId)
}
manageOrganizations(deleteOrganization, {
  description: 'This function removes an organization',
})


const updateOrganization = (organizationId = string, organizations = array, updateProperties = object) => {
  return organizations.map (organization => {
    return organization.id === organizationId
      ? { organization, ...updateProperties }
      : organization
  })
}
manageOrganizations(updateOrganization, {
  parameters: {
    updateProperties: 'the properties to update an organization with', // adds to parameters, doesn't overwrite
  },
  description: 'This function updates an organization',
})

It does fragment the documentation which isn't great imo but so long as we have good IDE support which tooltips that put everything together dynamically I think we could get away with it.

kee-oth avatar Mar 16 '22 17:03 kee-oth

@kee-oth I have limited exposure to the inner-workings of the ASTs and whatnot during the compilation process, but I'd think a rudimentary compilation step could be replacing the function documentor directly with the return function possibly without even currying it.

(this being for the documentation function w/ the built-in exec handler)

jzombie avatar Mar 16 '22 17:03 jzombie