Idea: Standard OCaml runtime type representation

Sure, but you’re describing situation where generic function with type representation won’t be also useful. I considered only the situation where ppx and type representation will be in concurrency.

That’s your right, but you’re still wrong. It’s pretty suprising that someone, who stand claiming he has not the knowledge nor the competence to implement such a system, thinks he can claim something about its supposed efficiency.

So let have a look at a very high level analysis of the system described by Daniel, and compare it to the already existing ppx system. Let name 'a ptree the parsetree data structure generated by the compiler after parsing the definition of the type 'a. This data structure contains too much information for the need of 'a typ, in such a way that we’re looking for a type that lies at this place in the type hierarchy of OCaml types:

'a ptree <: 'a typ <: 'a Type.Id.t <: unit

One possible path to find a data structure that is both necessary and sufficient for @dbuenzli needs is to start from the Parsetree data structure and remove everything that is not necessary (this data structure is obviously sufficient).

Now since the -> type operator is contravariant in its argument, we get for the example of equal function:

'a Type.Id.t -> 'a -> 'a -> bool <: 'a typ -> 'a -> 'a -> bool <: 'a ptree -> 'a -> 'a -> bool

In other word we can do more things with ptree than with typ, just because it contains more information, information that is not needed for the system we discuss.

Now, the system of ppx take an a ptree at its input, but don’t use it to generate a function a -> a -> bool, but generate an (a -> a -> bool) ptree. This ptree will then be consumed by the compiler to produce an a -> a -> bool function.

On the other hand, the 'a typ -> 'a -> 'a -> bool generic function will interpret the tree data structure a typ at runtime in order to produce an a -> a -> bool function.

So, from a performance point of view, we are comparing a native compiled language to an interpreted one. It seems clear and obvious to me that the first one is more efficient in general. :wink:

You are comparing apples and oranges here. Sid was talking about specifics of the data structure, and specific context needed to hand-write an implementation of a e.g. comparison function. You are talking about purely information contained in the structure of the type.

For example, how would a PPX generate an efficient, or even correct, comparison function for the Decimal.t data structure?

type sign =
  | Pos
  | Neg

type finite =
  { sign : sign;
    coef : string;
    exp : int
  }

type t =
  | Finite of finite
  | Inf of sign
  | NaN

As humans we know (in the context of the IEEE decimal floating point specification) that trying to compare NaN with any other number should immediately short-circuit to false. But a PPX can’t know that. If you try to analyze it purely from the perspective of information contained in types, you will reach an incorrect conclusion.

What don’t you understand in the following proposition ?

As @sid, you’re describing a situation where Fun.Generic will be useless.

Yes, because we are being pragmatic and saying that a runtime type representation will not be 100% useful 100% of the time. This has already been covered in this thread, many times. That doesn’t take away from the fact that it would still be useful for many, simpler, cases. E.g. generating a bunch of record types from a gRPC schema. And I want to print out the values of those types in my logs. With a PPX that generates the runtime type representation built-in, I just need to use that. This is just an example–before you say I can already use ppx_deriving_show for this, yes, we know. That’s not the point.

OK so the comparison between PPX and runtime type representations is the following as far as I can tell:

Runtime type representation:

  • Possibly easier to examine types than examining them in PPXs.
  • You’re writing normal functions rather than generating code, which is generally considered easier.
  • Not clear how you keep it up-to-date with an evolving language or even how to cover the entire surface area of the language. It’s one more thing that has to be synchronized.
  • Complexity seems quite high.

PPX:

  • Better performance: generated code vs interpreted code (interpreting the runtime types). This is critical for equality and comparison, but could also be important for serialization etc.
  • The mechanism already exists and has to be kept up to date with the compiler. Smart people are already taking care of it.
  • But most people don’t know how to write them.

I realize that nobody in mainstream OCaml wants to use quasi-quotations – really pervasive quasiquotations – but I feel I must point out that if you have sufficiently powerful quasiquotations, pattern-matching on types (or expressions, or patterns, or module-expression, etc) isn’t difficult and it is pretty transparent.

And you don’t need Camlp5 to make that the case.

The problem today is that current “standard” quasi-quotations packages allow you to “name” only those bits of the AST where variable-names (or module-names, type-names, etc) are allowed. You can’t name (for instance) a branch of a constructor-declaration. Writ large, this means that a PPX author must know a good bit of the AST type-collection, and that’s what makes the overhead of writing PPXes (and learning how to write them) so high.

But (again) none of this is necessary: sufficiently powerful quasi-quotation would make it possible so that nearly-all PPX authors could eschew nearly any knowledge of the actual AST types, sticking only to knowing the surface syntax and a little about where antiquotations are allowed.

People constantly talk about how hard it is to write PPXes. It isn’t. And again, I don’t mean using Camlp5 – I mean even using ppxlib. It just isn’t that hard.

Lets take up Equality generation by ppxes as an example. There are three cases:

(a) Equality auto-generated by ppx and programmer hand written Equality are the same. ppx-es did a great job and we’re happy here ! Yay !

(b) Equality auto-generated by ppx is just plain wrong. The ppx does not have an understanding of the domain so generates a wrong equality. Here the IEEE decimal floating point example by @yawaramin is a good one: Any NaN should not be equal to any other NaN but a ppx wouldn’t know that.

(c) Equality auto-generated by the ppx is correct but inefficient. Here, the ppx doesn’t have deep knowledge of the data structure/domain and know that there could be an early cutoff in equality comparison.

Let’s take up case (c). Lets have a VERY simple example with SIMPLE data types

type policy_applicant {
   usa_ss: int option, (* USA Social Security number *)
   foreign_id: string option, (* Foreign birth country code with foreign unique id *)
};

Let us say we have some insurance company. Let’s call it “Big Insurer Company” (ficticious example).

Let’s say “Big Insurer” sells insurance policies of all kinds: life, auto, house, travel etc… Before it issues a policy people need to apply for the policy with above identity data.

The Big Insurance company has the following fictitious though entirely plausible rules:

  1. Any person with a US social security number has to compulsorily submit it while applying for any insurance policy. We assume that everyone born in the US applying for a policy has a social security number.
  2. If you do NOT have a US social security number (maybe your are temporarily in the US and don’t have a social security number) you MUST provide your name of the foreign country where you were born and the unique ID that was assigned to you at birth there. For someone born in Japan, Big Insurer could save this in the system as “JP:12345ABC” where “12345ABC” could be that person’s unique id given at birth in Japan.
  3. If you have have a USA social security number AND were born in a foreign country you still need to provide BOTH pieces of data. This is maybe because Big Insurer would like to know the maximum possible details about its customers. US Green card holders will typically have both a social security number and a foreign id.

(In reality the data types are far more complicated in a real life applications and need multiple normalization and validations and are subject more real world business rules and exceptions etc.)

To generate Equality comparison for this data structure (maybe to see if you already exist in the insurance companies list of pre-existing customers), a ppx would naively generate code to compare both fields usa_ss and foreign_id. The equality comparison that is generated by the PPX here is correct

However a human writing the Equality code by hand knows that the moment the social security numbers match, the two data values are the same and you don’t need to compare the foreign_id field. Two persons are assumed to be the same if they have the same USA social security number. The ppx generated equality function would still check the foreign_id field for equality even if the usa_ss were equal!

The human written code here is correct and more efficient because we save comparison of an additional field, i.e. foreign_id (when the social security numbers are the same).

The point here is to show how a ppx could generate code that might not know that the comparison should be have an early cutoff. The human was able to generate more efficient code because they had domain knowledge of the data structure.

There would be so many more sophisticated, real life examples from Computer Science data structures etc. where naive comparison generated by ppx is correct but a more efficient version of equality is available because the human knows something special about the data structure. (This is the simplest example I could come up for this discussion in the time I had).

Firstly, I feel this comment is needlessly confrontational and unfriendly. Let me give you the benefit of the doubt and assume that you didn’t mean the above in any negative way at all.

To me, your specific criticism of me still doesn’t feel logical. Example: I don’t know how to build a car engine but I know how to drive it and can tell when a car is working efficiently, in good condition etc. all without that deep knowledge of a car engine.

Another example: I don’t have deep understanding of ppxes/refl/repr library in OCaml but I have a decent understanding of data structures, some understanding of code generation in general, and basic understanding of the mechanism how these ppxes work, how ASTs are traversed and have used these libraries as a user etc. I also have an academic background in Computer Science and have been programming for a very looong time. So I can make some educated guesses.

Coming back to this specific discussion we are having. You are free to give your point of view and lets have a friendly discussion and even correct me if I am wrong. I am ready to learn from anybody and I’m sure you are too! But please be friendly :slight_smile:

In conclusion, I still think I am correct about efficiency of ppxes. In the best case an auto-generated Equality (or any other operation) via ppx will be as efficient as a correctly written one by a human expert. At worst it will be totally wrong or less efficient.

Plus one to this.

I’ve recently been playing with Racket and writing macros in it, and honestly, it’s been a blast compared to the experience writing macros in OCaml.

I don’t think writing macros should really be that difficult — the problem with doing so in OCaml is just the sheer amount of boilerplate and AST bureaucracy required to get things running makes OCaml macros incredibly impenetrable, and easy to make mistakes.

I realise this is going slightly off-topic now, so maybe we should end this here, but maybe the real thing that OCaml needs is a better macro-writing system~.

Why did yo take so muck time to describe situations where type representation will be as useless as ppx?

Let me replay this for you.

You said:

This statement is not correct. I took the time to explain why it is not correct by giving you a counter example where this was not true (the whole insurance policy data structure example). It took long because explaining things step by step takes time.

You also didn’t didn’t seem to appreciate a scenario by @yawaramin where ppx-es do the wrong thing.

If you don’t feel it’s correct still its OK. @kantian lets not take this discussion further. It’s OK for us agree to disagree. Maybe you can ask some friends of your whose judgement you trust if there is any sense in what I’m trying to say. Otherwise let it be?

You don’t need, I perfectly know the question I tried to answer.

Originally, the question was this one:

You gave a wrong answer to this question (and you insist on your error), whereas I gave a complet answer whit semi formal proof of my claim. And it seems that @bluddy has acknowledged my answer:

So my work is done.

I thought that it may be useful to give a bit more details about the problem of type abstraction that I mentioned as needing a solution. It may explain, for example, why using the Haskell approach is not necessarily good enough in a ML language with a module system.

Consider a type 'a typ encoding the runtime representation of 'a. One obvious downside of foo typ is that it breaks data abstraction: if foo is an abstract type from you library, you have to choose between:

  1. not exposing a foo typ value, but then your library users cannot use your type foo as part of their own values and hope to use nice generic libraries (for, say, serialization or debugging)

  2. exposing a foo typ value, but then you broke data abstraction. Your users can inspect the definition of the type at runtime, make assumptions about what it is, use certain generic functions that depends on the internal details and end up making assumptions about how foo is defined. Next time you update your library and you want to change foo by a refactoring, their code breaks and everyone is angry.

Note that type classes by themselves (or show, eq etc.) don’t have this problem, this comes from the idea that you expose maximal information about your type. Exposing an instance of Ord or using @@deriving ord through the mli exposes some information about your datatype, but just a bit of it, and the library author controls which amount of information about foo they expose.

If you are lucky, the runtime type representation library allows to say “use the same representation as foo', and convert values back and forth in the following way”. This can be a way to protect some abstraction and preserve some backward compatibility. But (a) in most runtime type representation libraries this only works with isomorphisms, where you can convert back and forth without losing information, which is too restrictive to support most code evolutions, and (b) we are back to the performance costs of doing a deep copy of your value before applying the generic operation.

The problem that Grégoire Henry was working on was how to instead have a special case of runtime type representation that would be just opaque names, recovering data abstraction (at the cost of reducing the amount of functionality you can expect from an arbitrary runtime type, as some information may be missing): to the outside of my library I expose a foo type that is basically “actually I’m just the abstraction type Lib.foo”.

This brings back coherence problems, however. as the author of the library, I can see the real definition of foo and build a foo typ value that exposes it, and show an abstract foo typ to the outside. But then what happens if I mix the two representations, I mistakenly serialize with one and deserialize with the other? Maybe users call a generic function I provide with their own view of foo typ, and I end up working with the “outside” view of my values in code that sits inside the library and assumes the “inside” view. (This mirrors the “double vision” problem in the type checking of ML modules.)

Ideally there would be a way to have a single runtime type representation foo typ, whose interpretation depends on the current static typing context: when inspecting this value inside the abstraction boundary that defines foo I see the internal definition, from the outside I see the abstract type – or whatever the library author decided to provide. But how exactly should this work? That is the question.

Until your type class is called serialize I guess.

I’m not sure I see the limitation in a) here. It depends on what you do. Most of the time this problem does not happen at runtime (i.e. in the same program you always have a single representation there’s no need to support code evolution). The problem is with stuff you stored or sent away in a previous run with a previous representation which could come in your face in a run where the type has a new representation. In that case you just need to be able to decode the previous one and map it to the new one and sure if you were not careful that may not be possible but somehow that’s for the programmer to be. (You could also get representations from the future in a distributed system, but then here again if you can’t support it, you can’t).

Regarding b) that’s a bit pessimistic, your public representation may just be Fun.id or say for a record the record with a few fields elided, there no need to necessarily do a “deep copy” of everything.

In the end as soon as you want to serialize your abstract types you somehow break abstraction. I don’t see any other good way than somehow going the (versioned) “isomorphism” route. That is your type accepts to publish a few public representations. But then that’s nothing more than what your, say M.{to,of}_json, functions do. Except in a more useful way.

Actually if you really wanted you could maybe play with a public key infrastructure to validate/encrypt the serializations. But then… :–)

Can anyone provide a concrete example of a case where we want an abstract type but we don’t want to allow any inspection/introspection/runtime printing of the type’s values for debugging/serialization/logging purposes, for fear that people might use it to rely on the internal representation? I am having a difficult time wrapping my head around this scenario.

I’d suspect most secure data handling libraries would want to hide some parts of the internal implementation. (If “hiding” is what @gasche meant by “abstract”). For example, my Dirsp_exchange_kbb2017 interface has protobuf records for its implementation for wire communication and secure storage. But I would never (ever!) want the private key parts logged or printed to the console (just the public keys).

Of course, a silly or malicious library user could always expend effort to take the protobuf messages, serialize them to text and then print them. But I could protect against the “silly” library user by exposing pretty printer for everything, but that printed <protected> for the private key or token fields.

The more general class of “sensitive data” (personally identifiable information, security keys, auth tokens, etc.) will have the same pattern.

==> One technical requirement for sensitive data is the runtime representation should be optional. I think it trivially satisfied by what everyone has been talking about (correct me if I’m wrong!).

Yes, I have definitely custom-written a pretty-printer for production code to hide secret data. That is definitely one case where we don’t want to carelessly print out the data. I wonder if that is indeed what gasche was referring to. I somehow don’t think the OCaml team would be taking the position that ‘We will not implement something because it could allow you to carelessly expose private data’.

Two thoughts:

  1. Doing what you describe would be … highly detrimental to the reputation of the person who wrote such code. Every serious troubleshooter knows that they’re going to have to go into details that the authors of code didn’t envision: that’s what it means to be a troubleshooter, b/c if the authors were able to envision what you’re doing, they wouldn’t have written that buggy code. I have routinely hacked into Java bytecode, installed new functions to extract private data, and put that into deployed onsite customer enterprise systems (in UAT (== “user acceptance test”) not prod) in order to diagnose problems.

It’s a pretty standard technique. In that same vein, there are companies that sell boxes you can stick on a network, load SSL certs/privkeys, and then decrypt all the traffic. Again, very useful for debugging.

  1. Notwithstanding, my memory is that Golang’s network stack works hard to achieve this very thing. Since you cannot substitute your own network stack for Golang’s, you can’t stick in a version where certain private members have been made public, so you can debug. It’s … very suboptimal, but then again, since I never wanna touch Golang with a ten-foot pole, I don’t care much.

The real issue here is the idea that somehow programmers must be protected from stabbing themselves in the groin with hot pokers. I mean, yes, many systems try to prevent this, but somehow (just as you can write FORTRAN in any language) they manage to do it anyhow.

The correct way to prevent programmers from doing hideous and dangerous things, is code review. Truthfully, nothing else works. And in orgs with weak/nonexistent code review, all sorts of madness gets thru.

Leaving aside the idea that automatically generating a type representation for abstract types might expose secrets that would otherwise remain encapsulated— because that seems like an edge case to me— the more obvious scenario that springs to my mind is the one where you are simply trying to prevent what I call the Microsoft Word Problem.

When I worked in Core OS at Apple, it would regularly happen that various private frameworks and libraries would be found to have logic errors in them that produced very costly unexpected behavior. Correcting an error in a widely used application framework can be a dicey business sometimes, even when the correction entails making a change to the internals not exposed in the public interface. This can be especially problematic when you come to find that a critical business application, e.g. Microsoft Word, depends on the error in the logic not being corrected because its proper function depends on the interval and private layout of data structures that must change in order to correct the framework error. And you know that Microsoft is not going to force an upgrade on the 90% of your user base that relies on your next major OS not breaking their six year old version of Word.

Because, if there’s one thing I remember learning in that job, it’s that any visible data structure in the interface of a library will be forever immutable the moment it’s published. You will always run the risk that any change you make to it will break some downstream user who is misusing it horrible and unsafely, and they’re too big and important to force a breaking change on them. (Sometimes they’ll even duck-punch structures you don’t publish, but at least they mostly know how evil they are when they do that.)

You let the compiler automatically generate type representations for abstract types, then I guarantee you some programmer you’ve never met at some big company you can’t ignore will have the bright idea they can decompose your abstract data and force you to maintain its shape for them so their use of it won’t break.

I think that scenario is a lot more common than the need to hide the private crypto salami.

OK yes, I was wondering why nobody brought this up and then forgot about this critical point when listing the pros and cons. A runtime type representation completely breaks type abstraction.

@jbeckford, I think you should reconsider including refl. Include ppx_deriving, ppx_yojson etc instead. Heck, I would activate them by default in dune if I could.