Types as Interfaces
two-wrongs.com
two-wrongs.com
This is why ultra strong type systems end up being little more than academic toys - the payoff when you dial type safety up to an extreme doesn't match the associated cost.
Types and tests should meet somewhere in the middle because the same law of diminishing returns affects tests in reverse. They tend to be good at what the other is bad at and if you rely too heavily on one at the expense of the other they will start fraying around the edges.
Of course the problem comes when you have to interface some code which expects a "Vect n a" and you have a "List a", but that's solvable by simply checking the size (dynamically) and deciding what to do if your "List a" isn't of size exactly n. (Fail, or whatever.). Going from "Vect n a" to a "List a" is trivial, of course.
... but really, that's where most of the friction is: Boundaries between APIs where the types don't quite line up. (Which is often the case for other languages, tbf. We tend to just notice less at compile time since most languages are so bad at expressing constraints.)
You don't see many GAFAM products created in either, and that's because of the trade off OP talks about.
Rust is an example where a stronger type system has an associated payoff and it's being used all over.
There is not a single taxonomy that is universal, it ends up like the Celestial Emporium of Benevolent Knowledge that decides animals into:
those belonging to the Emperor,
those that are embalmed,
those that are tame,
pigs,
sirens,
imaginary animals,
wild dogs,
those included in this classification,
those that are crazy-acting,
those that are uncountable,
those painted with the finest brush made of camel hair,
miscellaneous,
those which have just broken a vase, and
those which, from a distance, look like flies.
Then you have a new use case.Regarding the ultimate utility of languages initially considered "academic," the languages Lisp, Haskell, Prolog, Scala, Erlang and OCaml would like to have a word ;)
Ada's generalized type contracts using subtype predicates work pretty well for this: https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
You can use it for something as simple as expressing ranges, or to represent types with arbitrary constraints, including types with discontinuities.
However, as a counterpoint, I'd suggest that encoding all known invariants as types may be prohibitively cumbersome and time-consuming - to the point where writing the program becomes more of an exercise in proofs rather than producing something useful. Often the invariants of a program are implied or can be easily inferred by the reader, which can actually make the program easier to understand for both the reader and the writer by leaving some things unsaid.
Of course, whether a program is easier to understand when its invariants are written down vs. when they are left to be inferred by the reader is probably a matter of circumstance, so different tools for different jobs and all that.
I've recently been writing a small program in Go - my first foray into the language - and I thoroughly enjoy how it gets out of my way. The lack of rigor is freeing if you don't stop to think about it too much.
But the difference is that with a dependent type, you get more guarantees (e.g. my ArrayLengthFive type could actually allow for arrays of length 20!). And the type checker forces you to prove the bounds (there could be a bug in my code when I create an ArrayLengthFive). And the dependent type allows other parts of the system to use the type info, whereas ArrayLengthFive is just an opaque type.
I do think there is room for both ways of working. In the string -> Email example, it's probably enough to parse your string and just call it an email. You don't need to try to encode all the rules about an email into the type itself. But there are certainly other cases where it is useful to encode the data constraints into the type so that you can do more operations downstream.
There is also the in-between Rust approach. Start with the user input as a byte array. Pass it to a validation function, which returns it encapsulated within a new type.
#[derive(Clone, Hash, Ord, Eq...)]
struct Email(Box<str>);
// validate UTF-8 +
fn validate(raw: &[u8]) -> Result<Email, EmailValidationError>;
What is annoying is the boilerplate required. You usually want your new type to behave like a (read-only) version of the original type, and you want some error type with various level of details.You can macro your way out of most of the boilerplate, with the downside that it quickly becomes a domain specific language.
1: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- validate(raw) -> void
- parse(raw) -> Email | Error
Of course the type signature is what seals the deal at the end of the day. But I am going to follow this naming convention from now on.I've seen attempts at solving each of those issues using types. I am not even positive they aren't solvable.
123 Somewhere Lane
Somewhereville, NY 12345
is a correctly formatted address but is almost certainly not one that physically exists.Validation that it exists isn't solvable in the type system because, as I mentioned, it is an event. It is only true for the moment it was verified, and that may change at any point in the future, including immediately after verification.
I'm curious, though, in how this argument does not apply to many other properties people try and encode into the types? It is one thing if you are only building envelopes and payloads. And, I agree that that gets you a long way. But start building operations on top of the data, and things get a lot more tedious.
It would help to see examples that were not toy problems. Every example I have seen on this is not exactly leaving a good impression.
For the most part, what I think people really want are branded types. F# has a nice example of unit, where a float can be branded Fahrenheit or Celsius or Kelvin, and functions can take one of those types as parameters.
You then have some function that can parse user input into a float and brand it one of those types. The compiler doesn't make any guarantees about the user input, only that the resulting branded value cannot be used where a different one is expected. Other good examples are degrees and radians, or imperial and metric, etc.
Depending on what you are doing, knowing at a type level that some number can never be negative (weight in pounds) can save you a lot of hassle. If one part of the system doesn't brand a value, and it feeds into another part of the system that is branded, you're stuck with extraneous runtime checks all over the place or a lot of manual unit tests. Instead, the compiler can point out exactly, and in every instance, where you assumed you had a validated value but instead did not.
I'm torn, as the examples from small programs make a ton of sense and I largely like them. I just cannot escape that every attempt I've seen at doing these in large programs is often fairly painful. I cannot claim they are more error prone than the ones that skip out on it. I can say they are far more scarce. Such that the main error condition appears to be to stall out on delivery.
Some invariants that are too complex to express purely in code, requiring numerous separate low level bookkeeping functions throughout the system, can be concisely expressed as a high level idea in natural language (wether as comments or at an external technical document).
Make sure that your developers are exposed to those documents, throw in a few tests for the most obvious cases to ensure that newcomers are learning the invariants by heart, and if they're competent you should not have problems but in the most obscure cases (that could appear anyway even if you try to formalize everything in code).
I eagerly await your implementation of a type describing my tax return. And to implement a compiler for the new programming language you'll inevitably have to construct, we're gonna need two types, one expressing all valid source programs, and the other all valid programs for the target platform.
Oh, can we also get updates to that for next year's taxes, new platforms, and other such future business needs? Of course the current requirements have to still remain supported.
I am open to the possibility that it can, but at the same time, I'd say if I draw the trendline of progress in this area out, it's not really all that encouraging.
If you want to see why it has remained only academic, spend some time with Idris and take notes on how many problems you encounter are fundamental to the paradigm versus problems that can be solved with a larger involvement of effort. You'll want to pick a task that fits into its library ecosystem already, and not something like "hey, I'll write a fully generic HTTP server that can be proved correct" on your first try. I seriously suggest this, it's very educational. But part of that education will probably be to tone down your expectations of this being a miracle cure any time soon.
I don't say this to celebrate the situation; it's a bummer. But if the situation is ever going to be solved, it's going to be solved by people viewing it clearly.
(One of the major problems I see is that when you get to this level of specification, you end up with everything being too rigid. It is true that a lot of programming problems arise from things being made too sloppy and permitting a lot of states and actions that shouldn't exist. But some of that slop is also what makes our real-world systems compose together at all, without the original creator having to anticipate every possible usage. The "richer" the specifications get, the more rigid they get, and the more the creator of the code has to anticipate every possible future usage, which is not a reasonable ask. While I'd like to have the option to lock down many things more tightly, especially certain high-priority things like crypto or user permissions, it is far from obvious to me that the optimum ideal across the entire programming world is to maximize the specification level to this degree. And those high-priority use cases tend to jump to mind, but we also tend to overestimate their size in the real world. If we are going to see this style of programming succeed, we need to figure out how to balance the rigidity of tight specification with the ability to still flexibly use code in unanticipated manners later, in a context where the original specifications may not be quite right, and I think it's pretty unclear how to do that. Tightening down much past Haskell seems pretty challenging, and even Haskell is a bridge or three too far for a lot of people.)
This is the story of improvements in type systems over the last 70 years. But the progress is very slow. So this is only slightly more optimistic...
I mean... we're going to end up with the first one. But a man can dream. And while I will cynically say we're going to end up with the first one, it is at least possible that we will then recognize there's a problem and pursue this second approach. But not until the first approach bites us, hard.
Yeah, scenario 1 will also probably still happen.
1. writes more proofs, focusing on the area I point at
2. makes small, atomic, incremental, suggestions to make it easier to prove something; once those pass proofs+tests, go back to #1
But hey, that would likely require some sort of artificial mental models and iterative reasoning, and not just slapping more compute+data together into a word generator...
x: 1..100
y: "yes" | "no"
I feel that there are lots of low hanging fruits when it comes to typing which would improve both program readability and correctness.The former is difficult to track through arithmetic operations.
X: 1 | 2 | 3 // … type N = 1 | 2
const x1:N = 1
const x2:N = 1
const sum:N = x1 + x2
Typescript (the version running on my box) considers this an error.Typescript is able to do this with strings when using templates.
type Suit = "C" | "D" | "H" | "S";
type Rank = "A" | "2" | "3" | "4";
type Card = `${Suit}${Rank}`;
const club = "D";
const ace = "A";
const aceOfClubs: Card = `${club}${ace}`;
Even though I have not declared the club or ace as a Suit or Rank, it still infers the Card correctly in the last line. This is a stronger form of typing than what you are settling for where 1..100 doesn't know its own range.This is the difference I'm referring to.
>There is nothing to suggest that 1..100 understands math.
That's like, your opinion man. I'd like it.
For operations/mutations where it's more complex to validate the inputs, you could assign the result to an unbounded variable, and then prove to the type checker that you're exhaustively handling the output before you assign it to a bounded variable. For example, multiply two unbounded numbers, store the result in an unbounded variable, then do "if result >= 1 && result <= 100 then assign result to var[1..100] else .... end"
[0] https://en.wikipedia.org/wiki/Refinement_type [1] https://goto.ucsd.edu/~ucsdpl-blog/liquidtypes/2015/09/19/li... [2] https://goto.ucsd.edu/~rjhala/liquid/liquid_types.pdf [3] https://ucsd-progsys.github.io/liquidhaskell/ [4] https://github.com/flux-rs/flux
I think a lot of the problem is that many of the constraints and acceptable values for data are not determined until well after first deployment. The "knot" comes in when you are explicitly changing data and code to add a feature that you didn't anticipate at the start.
This is a lot like schemaless databases. The flexibility of not having to fully specify the full schema does not mean that you don't benefit from specifying parts of it? Indeed, if you are indexing things, you have to specify those parts. But it is hard to argue with a straight face that schemaless tools don't have some strong benefits.
This is very similar to what Rich Hickey argued in "Simplicity Matters": https://youtu.be/rI8tNMsozo0?si=xTkpsLYTYh0jA5lB
Basically when you use a hash / JavaScript-style object rather than a fixed struct definition for your data (which amounts to a schema- less database in aggregate)
Easy addition of new properties
Co-existence of different versions of objects (newer objects will typically have more properties)
Introspection of properties and validation rules / reflectionI think this is a lot easier if you exclude the "methods" in my quote there. Since those almost certainly have the same general contract that would spread across things.
If you can upgrade all the data then you don’t need to worry about versioning,
Or you can do it like HTTP headers and hope for the best.
Common Lisp Object System also touched on all of these ideas years ago.
> The option to set a field to required is absent in proto3 and strongly discouraged in proto2.
Let me put on my straightest face.
Schemaless tools have no benefits.*
As soon as you start _doing_ anything with your data, it has a schema. You may not have declared it to your data store, to a specific piece of code, etc, but your application logic almost definitely makes assumptions about the shape of the data--what fields are and aren't present, the types of values in those fields, and what those values mean.
Every piece of your system must either accept undefined behaviour in the face of data which doesn't fit this implicit schema (there are very few non-trivial problems where you can define a meaningful, valid output for all possible inputs), or essentially treat the schemaless parts as hostile and assert its assumptions before every action.
The only thing you've done by going "schemaless" is distributing your schema across your systems and pushed the schema validation out to each thing interacting with the schemaless components instead of centralizing it.
* Yes, literally storing raw, unstructured data. But as soon as you need to _do_ anything with the data besides regurgitate it you're back to it having some sort of schema.
That is, you are not wrong in that most data stored is structured in some way; but you are ignoring a ton of the benefit of the schemaless world. As I pointed out, there are benefits to specifying schema. In parts, you /have/ to do so. Specifically if you want your DB to do any work on the data. Indexing/filtering and the like.
Now, you do hit largely on the benefits of the "schemaless" world. And that is that it is not undefined how it will store whatever schema you throw at it. You will have restrictions on keys, but otherwise it will store whatever you throw at it.
At large, this means you can skip out on a lot of the formality of specifying all schema shapes during development and can take a much more flexible approach in your code than you are forced to if the data layer can't work with you.
Useful types are a compromise between expressiveness and practicality.
In practice though, almost any program you'd want to write can be type checked, in the same way that few proofs get tied into Gödelian knots.
I get reminded of this everything I have to work with a certain large Typescript code-base of mine that makes heavy use of union and template literal types. The Typescript Language Server has a hard time with these types. Frequently, everything slows to a crawl in VSCode, and it take 10 minutes of more for intellisense to update after every type change, ouch.
Although in this case, I suspect it is Typescript's implementation of union types that is to blame (since other languages seem to handle complex union types with ease), this experience still shows that type checking can become quite expensive.
TotalProgram: possible in some, not in others, I think.
That's awesome! Thank you. I didn't know type systems were capable of expressing that level of sophistication.
Would HaltingProgram and NonHaltingProgram be expressible? I hope it's clear what I'm trying to convey, but the technical wording escapes me today.
Even history of "required", rather simple restriction on the value, is showing that putting that logic into type system is too much of a burden.
https://capnproto.org/faq.html#how-do-i-make-a-field-require...
The exact different between the two approaches is subtle. Technically you can fancier things with the framework exposing the optional values as set/not set, but in practice a lot of code is a lot simpler with default values.
Unfortunately, the more powerful the type system, the harder it is to make inferences. Generality comes at a cost.
A future programmer may be spending more time formally describing the invariants of the system.
That's a programming language. In other words, now your type system needs a type system.
I may be odd, but my programming problems are rarely due to the number of bananas or how bananas are represented, but whether I'm working with the correct bananas.
type FooBar = Foo & Bar
I doubt you will find a language where it is less clunky.Edit: Oh, I typed this on mobile, this was supposed to be a comment on another comment by posix_monad.
Typescript can't really be this language because it is impeded by having to work with the javascript runtime, which makes this task much much harder to do.
I am not a fan of encoding all sorts of correctness statements into a static type system. I'd much rather use a theorem proving environment for correctness, but one that is not based on type theory.
To quote from there: > Scala 3 has dropped some unsound and useless features to make the language smaller and more regular. It has added some new constructs to increase its expressiveness. Also, it has changed some constructs to remove warts and increase simplicity, consistency, and usability.
Some of the features they dropped because they were unsound were still useful (to me).
Typescript tries neither to make their typesystem (perfectly) sound, nor to make it as elegant as possible. That results in it to be very useful/pragmatic for everyday-programming tasks.
The ability to type most idiomatic javascript circa 2014. It's definitely a Faustian bargain.
Are you asking how am I sure that/if my specification is correct?
Are you asking how do I make sure I have no bugs without a proof?
Maybe you are asking something else entirely?
How do you use that to prevent errors?
Just rephrase your question as "How will a pervasive system of sanity checks help me prevent errors?", and I hope you agree that it kind of answers itself.
> I'd much rather use a theorem proving environment for correctness, but one that is not based on type theory.
Based on what then?
Nobody actually wants a language with a sound type system, unless they’re writing mathematical proofs. Any time you need to do anything with the external environment, such as call a function written in another language, you need an escape hatch. That’s why every language that aspires to real world use has an unsound type system, and the more practical it aspires to be, the more unsound the type system is.
Soundness is only a goal if the consequences of a type error are bad enough: your proof will be wrong, or an airplane falls out of the sky, or every computer in the world boots to a blue screen.
For everybody else the goal should be to balance error rate with developer productivity. Sacrificing double digits of productivity for single digits of error rate is usually not worth it, since the extra errors that a very sound type system will catch will be dominated by the base rate of logic errors that it can’t catch.
> For everybody else the goal should be to balance error rate with developer productivity. Sacrificing double digits of productivity for single digits of error rate is usually not worth it, since the extra errors that a very sound type system will catch will be dominated by the base rate of logic errors that it can’t catch.
I think you are missing my point.
You are merely looking at a single point in time. And yes, you are right - the balance you mention matters. But what also matters is the future. A language needs to be able to evolve. If it does not do that, it will eventually die and become replaced. If the typesystem is well made with good foundations, the language will be able to evolve and adapt faster and causing less problems for its users.
// This can go all the way back to the smallest common type:
listenForEvent("mouse", (event: {}) => { });
Typescript {} is a trap that means "any container of things is fine" (including objects), it's not the empty struct. One of the very ugly oddities of the language..
`Record<string, never>` or something like that is the closest equivalent to an empty struct.https://github.com/typescript-eslint/typescript-eslint/issue...
Formally, the usual notion of soundness is defined with respect to an evaluation strategy: a term-rewriting rule, and a distinguished set of values. For pure functional programs this is literally just program execution, whereas effects require a more sophisticated notion of equivalence. Either way, we'll refer to it as evaluating the program.
There are two parts:
- Preservation: if a term `a` has type `T` and evaluates to `b`, then `b` has type `T`.
- Progress: A well-typed term can be further evaluated if and only if it is not a value.
What we want to express here is an object with a map of properties (name to type):
string Type map
For the OOP minded: Map<string, Type>
And also compose those: type Foo = { "_foo", int }
type Bar = { "_bar", string }
type FooBar = mergeMaps Foo Bar
But at compile-time, of course.Have any languages achieved this? I know TypeScript can do some of these things, but it's clunky.
As an aside, Purescript is one of the most fun languages I've ever used for frontend work, and I lament that Elm seems to have overtaken it in the FP community.
[0]: https://hgiasac.github.io/posts/2018-11-18-Record-Row-Type-a...
You made me want to try purescript :)
Here's three recent papers on extensible records:
* https://arxiv.org/pdf/2108.06296 * https://arxiv.org/pdf/2404.00338 * https://dl.acm.org/doi/pdf/10.1145/3571224
I'm not claiming these are definitive in any sense, but they are useful guides into the literature if any reader wants to learn more. (If you are not used to reading programming language papers, the introduction, related work, and conclusions are usually the most useful. All the greek in the middle can usually be skipped unless you are trying to implement it.)
From what I read, gp only asks for a way to express the type, not a way to change/extend the type of something during runtime.
Elm's current implementation does this just fine.
type foo = < foo:int >
type bar = < bar:int >
type k = < foo; bar >
type u = < k; baz:int >
let f (x: <u; ..>) (\* the type annotation is not needed \*) = x#mActually getting values out of such a module-typed structure does involve some ceremony, however:
let f =
let module M = (val my_foobar) in
M.foo type HasFooBar s = (HasField "_foo" s String, HasField "_bar" s String) => sBut for some slightly complex reasons, most language designers find adhoc union types (which are required for this to work) a bad idea. See the Kotlin work related to that, they explicitly want to keep that out of the language (and I've seen that in other language discussions - notice how tricky it can be to decide if two types are "the same" or whether a type is a subtype of another in the presence of generalized unions) except for the case of errors: https://youtrack.jetbrains.com/issue/KT-68296/Union-Types-fo...
But these are obviously not equivalent: the first type is a map where all values are either strings or ints, and the second one is either a map where all values are strings, or all values are ints.
If that's confusing, consider: {foo: 1, bar: "2"}. It satisfies `Map<String, String | int>` but not `Map<String, String> | Map<String, int>`.
(In fact, the latter is a subtype of the former.)
P.S. You also seem to have misunderstood the toplevel comment about modeling record types as maps, as being about typing maps that exist in the language.
EDIT: ok, I just wanted to say that one type, `Map<String, String | int>`, is a supertype of `Map<String, int> | Map<String, String>`, so if a function accepts the former, it also accepts the latter. They're not equivalent but you can substitute one for the other (one way only) and still perform the same operations (always assuming read-only types, if you introduce mutation everything becomes horrible).
I was trying to emphasize how having type "combinations" ends up causing the type system to become undecidable as you end up with infinite combinations possible, but as I haven't really gone too deeply into why, I am having trouble to articulate the argument.
How are these equivalent? Wouldn't the latter result in {foo:1, foo:"two"}, where the former wouldn't?
On the other hand, thanks to the loose "duck typed" nature of templates, you can write functions that act on a subset of the fields of a type (even type checking it with concepts) while preserving the type of the whole object.
type Foo struct {
foo int
}
type Bar struct {
bar string
}
type FooBar struct {
Foo
Bar
} #include <iostream>
#include <string>
#include <type_traits>
// Concept to check for 'foo' field of type int
template <typename T>
concept HasFooInt = requires(T t) {
{ t.foo } -> std::convertible_to<int>;
};
// Concept to check for 'bar' field of type string
template <typename T>
concept HasBarString = requires(T t) {
{ t.bar } -> std::convertible_to<std::string>;
};
// Generic function that accepts T with either 'foo' or 'bar'
template <typename T>
requires HasFooInt<T> || HasBarString<T>
void printField(const T& t) {
if constexpr (HasFooInt<T>) {
std::cout << "foo: " << t.foo << std::endl;
} else if constexpr (HasBarString<T>) {
std::cout << "bar: " << t.bar << std::endl;
}
}
struct Foo {
int foo;
};
struct Bar {
std::string bar;
};
struct FooBar {
int foo;
std::string bar;
};
int main() {
Foo s1 { 42 };
Bar s2 { "hello" };
FooBar s3 { 100, "world" };
printField(s1); // prints: foo: 42
printField(s2); // prints: bar: hello
printField(s3); // prints: foo: 100 (prefers foo due to order of checks)
return 0;
} type Foo interface {
Foo() int
}
type Bar interface {
Bar() string
}
type FooBar interface {
Foo
Bar
}
Then functions that accept a Foo will also happily take a FooBar. Does not solve the problem of passing a FooBar[] to a function that expects Foo[] but that can be solved with generics or a simple function to convert FooBar[] to Foo[].I like the beauty of an expressive type system as much as the next guy, but is there any scenario where:
type FooBar = mergeMaps Foo Bar
Is better for solving real-world problems than type FooBar = { "_foo", Foo, "_bar", Bar }
or whatever?”The MLs” solved it decades ago. Standard ML has very nice record types.
type Person = User DESELECT password
type Invoice = Customer JOIN Inv type Person = Omit<User, 'password'>
type Invoice = Customer & { invoice: Inv }
// or
type Invoice = Inv & { customer: Customer } type foo_bar = < _foo : int; _bar : string >
For a family of types matching any object with those methods, I think you can write something like type 'a has_foo_bar = < _foo : int; _bar : string; .. > as 'aThis is the kind of problem you face in your first year working, no? I am honetly curious what others think. Do you have trouble deciding when to use an interface (assuming your language has that), or a type wrapper (I don't think that's the brightest idea), or a function to extract the field to sort by (most languages do that)??
Nobody said this.
There is value in thinking about things like this sometimes, because it has long-term consequences for the projects we work on. Even if you're a "professional" programmer (whatever that means), it's valuable to go back to beliefs and knowledge you've established long ago to evaluate whether to change them in the face of new experiences you've made since the last time you've thought about it.
If you think "professional" programmers don't get this sort of thing wrong in some form or another, I have a bridge to sell you.
If you do that, you'll have run into this sort of decision very early in your career, and hopefully will understand the best way to handle it, which IMHO just depends on the language (because certain features lead to different optimum solutions) and the culture around it. But sure, I am happy to discuss fundamental topics like this, that's why I am engaging and asking what others think.
I would say, yes. So does everybody else. If you don't believe it, I don't think you appreciate the depth of these problems.
I am reminded of Eric Meijer saying that the interface (in the Java sense) part of the contract between types is the least interesting part. What is important are their algebraic properties, which can be modeled using functional types and other bunch of advanced type-theoretical ideas (like linear types for instance).
Modelling stuff with types is not easy, it's an art.
Typically there are subtle trade-offs and compromises which only prove themselves to be useful/detrimental as the software changes over time. You can place bets based on experience but you can only really be cocky about your choices when looking back not looking forward
You're imagining things (uncharitably); that's not in the comment you replied to.
I did read a lot into the '??' punctuation which might not have been intended.
interface Timestamped {
timestamp: UTCTime;
}
interface Msg {
sender: PlayerId;
}
class Quote implements Timestamped, Msg {
timestamp: UTCTime;
sender: PlayerId;
}
Why is this so hard in Haskell? It doesn't have interface polymorphism? class HasRecipient a where
get_receiver :: a -> PlayerId
which adjusted to your example would be class Timestamped a where
timestamp :: a -> UTCTime
The problem with this approach is that you'll have duplicated data on all instances. In your example, `Quote` has the fields `timestamp` and `sender` in order to satisfy with `Timestamped` and `Msg`. If you had several classes like `Quote` and interfaces like `Timestamped` then you would end up with a lot of duplicated code.IIRC you can have traits automatically implement this sort of behavior with a centralized implementation.
The challenge is composition. Adding Timestamped to something in a static, type-checked way, but not modifying the source code of that something.
> It doesn't have interface polymorphism?
This is besides the point, but its interface polymorphism is static, not dynamic. If you have a List which is IMappable, and you do some IMappable operations on it, in most OOP languages you get back an IMappable, but in Haskell you get back List.
Who says that and what does it even suppose to mean?
Complex types and objects don't exist.
Embrace Mereological Nihilism.
It's fun at meetups to tell everyone your programming paradigm is Nihilism.
In the philosophic position of mereological nihilism only "simples" exist when discussing objects, etc, simples are akin to byte types for sure and primitive types maybe.
Nothing complex, like objects, higher level types, etc. exist.
Composition of simples in time and space is what determines everything other than simples. Anything that isn't a simple is just emergent from the space/time arrangement of the simples.
So if the implications are that nothing is real outside of time/space/simples, then calling something higher level a "type" is to label something as discrete and real when it isn't, so now you are using language and models that are wrong to reason about things, which means your model will have frustrating drift from reality that you can't rectify.
That's where schemas come in, we are just labeling common arrangements of simples for semantic reasons but they don't really exist so they shouldn't be elevated to the position of a type as that is a wrong abstraction and causes divergence and mess.
interfaces describe behavior, types describe shape and structure. the difference is subtle but important.
Shape and structure are behavior. There's more to behavior than "abstractly produces X result". Behavior is "produces X result in Y form given Z in W form".
They shouldn't be. Conflating two things that can be separated is just compounding complexity. You don't need to know how a database lays out memory or the structure behind a web server.
You do, however, need to know what logical columns are in a table and the types of those columns to be able to effectively query against the table. And you need to know what the types of the inputs to a query wrapper function are to be able to call it properly.
Memory layout has nothing to do with type, because physical memory layout is completely separate from the semantic logical layout and form represented by the bits. You now appear to be the one conflating two unrelated things.
That something is treated as an integer fundamentally matters to its use. That the thing comprises some number of adjacent bits in big/little-endian arrangement is a very unrelated implementation detail.
That's the interface.
Memory layout has nothing to do with type,
So float, int, unint64, int8 and a vector/array don't have specific memory layouts?
the semantic logical layout and form represented by the bits
This is nonsense and doesn't mean anything.
The memory layouts don't need to be known to the user. Different hardware architectures can have the concept of floats and ints and code can be written using them the same way while the underlying representations are distinct. I promise that IEEE754 is not the only way to represent floating point values handed down from god to man on a golden scroll.
> > And you need to know what the types of the inputs to a query wrapper function are to be able to call it properly.
> That's the interface.
Funny, I recall you not very long ago saying "interfaces describe behavior, types describe shape and structure". Now you acknowledge that the interface necessarily includes the types of the arguments?
This isn't relevant one way or another.
Different hardware architectures can have the concept of floats and ints
This also isn't relevant. Data types aren't expected to be cross platform unless they are specifically made for that, like file formats.
Funny, I recall you not very long ago saying "interfaces describe behavior, types describe shape and structure"
I never said that.
Also just because two things work together doesn't mean they are the same thing.
> This isn't relevant one way or another.
What needs to be known to the user of an interface is the most relevant aspect of describing that interface.
> Data types aren't expected to be cross platform unless they are specifically made for that, like file formats.
It's going to be difficult to reconcile that position with https://en.wikipedia.org/wiki/Abstract_data_type
Data types can optionally specify machine representation but need not. They specify behavior (i.e. possible values and operations) first and foremost. In programming, nearly every use of data type is in the abstract, entirely separate from its hardware representation. The "Int", the "Bool", the "2", the "UTCTime" being specified in TFA don't care about bit arrangements. They're describing valid values and operations, not hardware.
> I never said that.
You're right. Apologies. It was the other poster at the top of the thread. But it still seems apt to the thread and to your specific portrayal.
Not every scenario cares about bit arrangements but they are still there.
You seem to just be shifting around and coming up with new, more abstract arguments that drift further from whatever point you originally had.
You said
"Shape and structure are behavior."
One is data, one is execution. These are two different things.
The more they are conflated together, the more problems people have with their programs.
Not separating them and letting them mix is a huge part of bad software architecture and leads to lots of unnecessary complexity.
This text should not fascinate a programmer but create frustration of two types: (1) Frustration on one's lack of small pieces knowledge (2) Frustration that NOW you will not have the chance to invent this on your ow; your creative process takes damage.
Out of which the second type should be the one that makes majority of cases.
ALSO this should cause one to have respect of form: "hey, this programmer was probably at least smart enough to figure out this alone."
Perhaps most polite woule be to, for every text, in the situation to have notice at beginning: "For programmers who already have thought about what would happen were you to add X to Y but so that Z enough to probably not to get new ideas in this context."