jamiebuilds / ghost-lang

:ghost: A friendly little language for you and me.
302 stars 10 forks source link

Privacy #35

Open jamiebuilds opened 5 years ago

jamiebuilds commented 5 years ago

I've been thinking a lot about privacy as it pertains to functional programming.

Generally I prefer the idea of privacy through inaccessibility. AKA, if you don't want someone to have access to something, just don't expose it anywhere that could be accessible.

For example, (ignoring privacy details for a second) in JavaScript we could write a User class like this:

export class User {
  constructor(id) {
    this.id = id
  }

  createApiRequest(method, endpoint, body) {
    return fetch(`/api/users/${this.id}/${endpoint}`, { method, body })
      .then(res => res.json())
  }

  fetchPosts() {
    return this.createApiRequest("GET", "posts", {})
  }
}

Syntactically this implies that you could call user.createApiRequest() and it's only by the language adding a new feature for privacy (In JavaScript: #private members) that it becomes clear you aren't meant to access something.

However, you could write the same code using just functions and exports:

function createApiRequest(userId, method, endpoint, body) {
  return fetch(`/api/users/${userId}/${endpoint}`, { method, body })
    .then(res => res.json())
}

export function fetchPosts(userId) {
  return createApiRequest(userId, "GET", "posts", {})
}

Here you get privacy just by never syntactically exposing (or rather export-ing) anything.

However, there are still places where not exposing something to achieve privacy is more effort. For example, take this recursive sum() function:

let sum = fn (numbers: Array<Number>, total = 0) {
  if (numbers.length) {
    sum(numbers[1...], total + numbers[0])
  } else {
    total
  }
}

Here, total is exposed by being part of the function signature, but for the purposes of this example we don't want that to be an exposed part of the API.

So instead we have to wrap the sum function with another function:

let sum_ = fn (numbers, total = 0) {
  if (numbers.length) {
    sum_(numbers[1...], total + numbers[0])
  } else {
    total
  }
}

let sum = fn (numbers) {
  sum_(numbers)
}

This works, but is kinda gross, and I'm afraid people wouldn't reach for this right away. Possibly instead writing their code in an iterative fashion so they can have internal state.

One option I came up with was adding a feature to Ghost for private function arguments:

let sum = fn (numbers, private total = 0) {
  if (numbers.length) {
    sum(numbers[1...], total + numbers[0])
  } else {
    total
  }
}

In this case, you are allowed to use private arguments when calling the function inside of itself, but not from any other function/context.

This gives you two different function signatures, one from within the function and another from outside. Which is no different from how classes are in most languages, but is (I think) unusual for Ghost.

I'll continue thinking about this, but for now I'm defaulting to not adding a language feature unless I think it dramatically improves things.

SCKelemen commented 5 years ago

How would this be any different than overloading a function and exposing only one of the overloads? The exposed overload could just call the internal overload with the default parameter.

jamiebuilds commented 5 years ago

It's not any different, but adding overloading is a whole other featureset that I don't feel great about

SCKelemen commented 5 years ago

Go takes your exporting suggestion one step further, and instead of introduce an export keyword, it uses the first letter of they Identifier to indicate accessibility. PascalCase is enforced for exported identifiers and camelCase is enforced for unexported identifiers. Have you considered something like this?

I'm not advocating either way, just bringing up something to be considered.

jamiebuilds commented 5 years ago

I've been on the fence about identifier naming having special meaning. I like stuff like _unused vs used because that has no real effect. But to change semantic meaning based on an identifier name seems like too much.

Vaguely:

Yes: Semantics enforcing identifier name No: Identifier name causing semantics