Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update README.md to reflect #53 #54

Open
pocesar opened this issue Dec 2, 2019 · 5 comments
Open

update README.md to reflect #53 #54

pocesar opened this issue Dec 2, 2019 · 5 comments
Assignees

Comments

@pocesar
Copy link
Contributor

pocesar commented Dec 2, 2019

although the PR #53 introduces the Core.App(), and using app.root.map idiom, it doesn't show to use the "mapped" stack to existing CDK code (as in using new new cdk.Stack([what goes here now]) and what goes in new Bucket([here instead of Build<cdk.Stack>]).

Everything should go inside map? Or the return value from map should be used again using map((s) => new Bucket(s)) on the rest of the code? When to use chain? What does ap do?

to be clear: the array method map gives the wrong impression that root.map is an iterator with side effects, although it's just one item. but after reading the code, learned that map is actually a function that provides a param and outputs another wrapped Build.

Also, would be good to export the inner Build type, just in case. (from the index main export)

EDIT: after reading the internals in more-depth, I noticed that Build is really powerful, and while the abstractions of the AWS services are awesome, I think it deserves a "docs" for him, because it needs to get used to it to juggle around CDK constructs and Punchcard abstractions

@sam-goodwin
Copy link
Owner

Oh hey! Sorry, I haven't been checking this in a while - thanks for trying punchcard and providing feedback.

You're absolutely right that Build needs more documentation. For now, Build is built on fp-ts's Monad: https://gcanti.github.io/fp-ts/introduction/core-concepts.html so perhaps their documentation can help.

The way I think about Build is that it lazily expresses computations that happen in the Build-time phase of an application. So, to interact with existing CDK code, you only need to "map into" existing Build context or create one for yourself.

E.g. without using `Core.App:

const app = Build.lazy(() => new cdk.App())
const stack = app.map((app: cdk.App) => new cdk.Stack(app, 'my-stack'));

This is a lot like ordinary CDK code:

const app = new cdk.App();
const stack = new cdk.Stack(app, 'my-stack');

Except that execution of the CDK code is delayed until the entire Build tree is executed during CDK synth. This layer is designed so that arbitrarily complex CDK code can be used without impacting the code that runs in Lamdba - e.g. interacting with the OS to create files or build code and Docker images etc. Before Build existed, everything was executed all the time - so your lambda function would be calling things like new iam.Role() and table.grantRead(role), which are wasteful and fragile when running within the Lambda Function. Things liek CDK Assets, for example, would break punchcard because they synchronously call out to the file system :(

In other words: Build and Run improve the portability and interoperability between Punchcard and the CDK by encapsulating computations lazily.

RE: the difference between map and chain:

class Build<T> {
  <U> map(f: (t: T) => U): Build<U>;
  <U> chain(f: (t: T) => Build<U>): Build<U>;
  // chain is also known as flatMap in other functional languages, like Scala or Haskell
  // even Java's Stream (which is also a Monad) has flatMap: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#flatMap-java.util.function.Function-
  <U> flatMap(f: (t: T) => Build<U>): Build<U>;
}

Use map when you don't need to interact with any other Build instances. E.g. creating the Stack from the App (above) didn't need chainbecause it can create the Stack from the App instance passed to its map function.

Use chain when you need to link with other lazily constructed values.

const myTable: Build<dynamodb.Table>;
const myRole: Build<iam.Role>;

const myFunction: Build<lambda.Function> myRole.chain(role => myTable.map(table => 
  const f = new lambda.Function(..,..,{
    role
  });
  table.grantRead(f);
  return f;
))

If we used map here instead of chain, then the type of myFunction would be Build<Build<lambda.Function>>. chain flattened that for us, which is also why it's often called flatMap.

@pocesar
Copy link
Contributor Author

pocesar commented Dec 17, 2019

awesome, thanks, I'm already publishing stuff about punchcard even if it's considered unstable and testing it for an MVP like I said (I just can't make it public atm, but I'll eventually), but that's how eagerly excited I am about the whole concept (CDK itself already kick-started my interest). the Build / Run is pure genius, I might add.

@pocesar
Copy link
Contributor Author

pocesar commented Dec 21, 2019

okay, I tried to do the following:

import getResource from './mycode'

/*...*/

const api = stack.map((s) => {
  const certificate = Certificate.fromCertificateArn(
    s,
    'certificate',
    certificateArn
  )

  return new p.ApiGateway.Api(stack, 'api', {
    domainName: {
      domainName: 'domain',
      certificate
    }
  })
})

const res = Build.resolve(api).addResource('res')

res.setGetMethod({
  integration: endpoint,
  responses: {
    /*...*/
  },
  request: {
    /*...*/
  },
  handle: async (r) => {
    try {
      return p.ApiGateway.response(p.ApiGateway.StatusCode.Ok, {
        result: await getResource(r.code)
      })
    } catch (e) {
      return p.ApiGateway.response(p.ApiGateway.StatusCode.InternalError, {
        result: null,
        error: e.message
      })
    }
  }
})

obviously didn't work, because I can't use Build.resolve outside of map/chain (and fails when deployed with attempted to resolve a Build value at runtime which is totally true (awesome error reporting by the way 👍 ). should I put everything inside? Because if I use chain, it gives me

image

to make it happy, add a Build.of, but the end result is the same for when using map. The reason I need the "api" variable is to assign it as an alias for a subdomain as a RecordSet using AddressRecordTarget.fromAlias(new ApiGateway(Build.resolve(api.api))) which works, for Route 53, it's in another stack.map later on the code, but doesn't work for the Build value error above

my first attempt was putting Certificate.fromCertificateArn(Build.resolve(stack)) outside, alongside new p.ApiGateway.Api, but the error is the same (and justifiably)

EDIT: ok, I think I see now how it should be. nothing from punchcard should go inside map, because it won't work at runtime, since map / lazy, aren't evaluated when running on lambda. it's making more sense the more I use it, but need some mental exercise

@pocesar
Copy link
Contributor Author

pocesar commented Jan 8, 2020

not related directly to the issue, but I ran across a documentation website generator, https://github.com/facebook/docusaurus might help with documenting this gem of a framework

Related:
https://blog.axlight.com/posts/react-tracked-documentation-website-with-docusaurus-v2/
https://github.com/TypeStrong/typedoc

@sam-goodwin
Copy link
Owner

sam-goodwin commented Feb 5, 2020

Sorry I haven't responded to your issue yet. It looks like you're getting confused by the Punchcard constructs p.ApiGateway.Api and the CDK's.

p.ApiGateway.Api should be constructed outside of the Build and refrain from ever calling Build.resolve, especially in the global module-load scope.

I don't have a better solution at this time though - I hope you don't mind waiting until I get round to re-implementing the API gateway stuff. It is by far the most neglected part of Punchcard right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants