2

The following scenario shows an abstraction that seems to me to be impossible to implement declaratively.

Suppose that I want to create a Symbol object which allows you to create objects with strings that can be compared, like Symbol.for() in JavaScript. A simple implementation in JS might look like this:

function MySymbol(text){//Comparable symbol object class
  this.text = text;
  this.equals = function(other){//Method to compare to other MySymbol
    return this.text == other.text;
  }
}

I could easily write this in a declarative language like Haskell:

data MySymbol = MySymbol String

makeSymbol :: String -> MySymbol
makeSymbol s = MySymbol s

compareSymbol :: MySymbol -> MySymbol -> Bool
compareSymbol (MySymbol s1) (MySymbol s2) = s1 == s2

However, maybe in the future I want to improve efficiency by using a global registry without changing the interface to the MySymbol objects. (The user of my class doesn't need to know that I've changed it to use a registry)

For example, this is easily done in Javascript:

function MySymbol(text){
  if (MySymbol.registry.has(text)){//check if symbol already in registry
    this.id = MySymbol.registry.get(text);//get id
  } else {
    this.id = MySymbol.nextId++;
    MySymbol.registry.set(text, this.id);//Add new symbol with nextId
  }
  this.equals = function(other){//To compare, simply compare ids
    return this.id == other.id;
  }
}
//Setup initial empty registry
MySymbol.registry = new Map();//A map from strings to numbers
MySymbol.nextId = 0;

However, it is impossible to create a mutable global registry in Haskell. (I can create a registry, but not without changing the interface to my functions.)


Specifically, these three possible Haskell solutions all have problems:

  1. Force the user to pass a registry argument or equivalent, making the interface implementation dependent
  2. Use some fancy Monad stuff like Haskell's Control.Monad.Random, which would require either foreseeing the optimization from the start or changing the interface (and is basically just adding the concept of state into your program and therefore breaks referential transparency etc.)
  3. Have a slow implementation which might not be practical in a given application

None of these solutions allow me to sufficiently abstract away implementation from my Haskell interface.

So, my question is: Is there a way to implement this optimization to a Symbol object in Haskell (or any declarative language) without causing one of the three problems listed above, and are there any other situations where an imperative language can express an abstraction (for example an optimization like above) that a declarative language can't?

luqui
  • 59,485
  • 12
  • 145
  • 204
while1fork
  • 374
  • 4
  • 15
  • 3
    Oh yes, if you don't allow to promote the interface to monadic (as in your no. 2), then imperative languages can express all sorts of abstractions that declarative languages can't. For example, the abstraction of a mutable variable. – luqui Jun 07 '17 at 21:20
  • 6
    observe that if you don't implement interning correctly, that you will lose referential transparency. E.g. if you exposed the interned id, it will depend on evaluation order. Say, if you serialized your symbols, your file output might change due to what should have been correct refactors. So when I'm in rigorous mind, I say *that dependency must be modeled accurately* -- promote to a monad or other structure that captures those effects. When I'm in practical mind, sidestep transparency, throw caution to the wind, and use `unsafePerformIO`, hoping I did it right. – luqui Jun 07 '17 at 21:36
  • 9
    It seems as though you want to have your cake and eat it too -- you want to use an abstraction which is not guaranteed to be pure as if it were guaranteed to be pure. – luqui Jun 07 '17 at 21:39
  • 3
    See [this package](https://hackage.haskell.org/package/intern) for an example of packaging up the unsafe code in a safe interface. At the end of the day, strong type systems are supposed to _limit_ the number of programs accepted by the compiler (even correct ones). The interesting followup question would really be: "are there problems for which Haskell's type system prevents me from making a solution as efficient at runtime as language X". – Alec Jun 07 '17 at 22:57

1 Answers1

4

The intern package shows how. As discussed by @luqui, it uses unsafePerformIO at a few key moments, and is careful to hide the identifiers produced during interning.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380