Inflation can exist because of a lot of things: natural loss of value, resource scarcity, monetary policy, greed, etc. And it's even harder to make sense of with fiat currency.
> Seemed like a vicious cycle.
The issue is inflation and deflation both tend to be positive feedback loops. Inflation can promote behavior that promotes inflation. Deflation can promote behavior that promotes deflation.
Note that I use "tend to" and "can promote". It's all based of off assumptions on how people value things and their behaviors, as is all economic models.
> why prices HAVE to keep going up
It really doesn't have to. We do so because economic models show that we should because of the way we behave. But, we also behave the way we do because of the economic systems that we've designed.
Prices have to keep going up if you want a system that promotes endless consumption and growth in consumption.
It also lets you have a "non-zero-sum" economy, where it appears everyone is making a "profit". But, in reality it isn't.
Yes, the laws of thermodynamics and laws of economics are empirical laws. But, the laws of economics are derived from human values, which are inherently subjective.
You state the choices as “illogical”, but those choices can be logical based off a different set of values.
Similarly, if you have a different set of axioms, you can build a different reasonable system on it.
It's like Euclidean geometry and Non-Euclidian geometry. They are both valid systems based off of different axioms. Similarly, the different economic systems are valid based off of different set of societal values.
You can also compare it to the ideal gas law. It's a law, but is based of a hypothetical ideal gas. Similarly, the economic laws are based off of a hypothetical society. The ideal gas law does not hold in all conditions, and economic laws do not hold in all conditions.
The economic laws are meant as tools to predict behavior. But ironically, we end up modifying our behaviors to fit the laws, and we weaponize the usage of "economic laws" to control the behavior of others.
We have economists complain how "that economic system doesn't work". Yes, it doesn't work with the laws that define your economic system, but it works with a different set of laws. We have people say, "that doesn't make sense because of X law". It's the other way around. The "law" doesn't make sense, because I value something different.
I don’t see how it’s an improvement over C# structs. C# structs are value types so they are copied when assigned to a variable like primitives. There is no ambiguity because it’s a struct.
To avoid copying you have to explicitly declare a ref variable/parameter.
You can get the same immutability as value classes by using ‘readonly struct’s or ‘readonly record struct’s.
Java value classes are stranger because they are heap allocated by default and are only flattened/scalarized/stack-allocated when certain conditions are met. It’s the same as a class, but with extra restrictions, so that the JVM can possibly optimize memory layout at runtime.
The C# equivalent to Java ‘value class’ would be a class with a struct encapsulated for data. The data is flattened and allocated on the heap like Java. Similarly, escape analysis could stack allocate the class at runtime, and they can be scalarized like C# structs.
Java ‘value class’ only flattens if the total size of the class data fits within an atomic read/write op. You can force it to flatten, but you may have tearing like C# struct.
I hate to say it. But thats user error. The struct paradigm is different from classes. Structs are meant to be plain-old data types; simply a typed span of memory.
Structs are values, classes are entities with encapsulation.
The shape of the state would be structural. Whether or not the data in that shape is valid is behavioral.
Structs are useful when working with spans of memory.
Another example of a good usage of struct is Guid, which is 128 bits of data packed together.
The C# equivalent to Java ‘value class’ would be a class with a struct encapsulated for data. The data is flattened and allocated on the heap like Java. Similarly, escape analysis could stack allocate the class at runtime.
This is what makes the nuances of Valhalla's Value types so compelling. The idea are granular structs with "Integrity by default", where you can selectively give up constraints on class design to get performance characteristics.
Structs in most languages simply bunch a couple constraints together to get another set of performance benefits, but there's no law stating that they couldn't be singled out. In the design of Valhalla, it states that types can come in 4 buckets:
2: Value Based classes (no mutability, but full integrity and dense memory layout)
3: Implicitly constructed values (forced empty default constructor for swift bulk array initialization)
4: Tearable Values (No cross-field integrity during runtime for parallel access)
And I bet that for a vast majority of developers, #4 will come to a shocking surprise, thinking "values are threat safe" because they are told to use immutables.
This way of splitting up structs is the real interesting part of Valhalla, but this shitty AI-generated article buries everything interesting.
Imo #4 is why it’s not that useful. If the data is larger than an atomic read/write op the data isn’t flattened and it’s a regular object with value equality and immutability.
You have to opt into force flattening, and then it’s the same as a struct, except it’s still heap allocated without escape analysis. You still have to implement synchronization to prevent tearing.
Static code analysis can give you a warning for potential tearing of structs.
DotNext.Threading provides Atomic<T> to enable high-performance atomic operations on structs without heap allocation.
The design of value classes just seems counteractive to its purpose: memory management. If I want to manage contiguous blocks of memory, let me manage contiguous blocks of memory. If I want to allocate something on the stack, let me allocate something on the stack.
The paradigms of struct vs object are too different and they’re trying to combine them into one.
Thanks, I'm always happy to learn more about .net and the CLR.
Regarding #4, is this actually a done deal? I haven't dug into the JVM specifics, but I thought they would avoid allocating objects. And for now they just want to get the model right, while continue to optimize as time moves on. I think that's the right approach.
I actually see this way less critical because if you truly have performance-critical usage of structs, you know what you are doing. And if you know what you are doing, you will know about opting-in.
And for everything else? I think it's nice to have a range of benefits that come from having a value type without handing a gun to a monkey. Because the feature will be misused by people that don't know about tearing, thinking "value" is a free performance upgrade. And I do believe that it is the right mental model to reason about it.
I just don't see the huge issue. If the CLR has a way to provide atomic access to non-tearable structs, surely the JVM can too? We are talking CPU instructions here after all. Or am I missing something?
Yeah, that’s a different issue. Statically-typed languages, including type-erased languages, are fine on the CLR. Dynamically-typed languages are a different beast.
I suppose DLR would be comparable to GraalVM/Truffle.
The difficulty of implementing a dynamically-typed language directly on the CLR and JVM are about the same. Though, it would probably be more efficient on the CLR with access to lower level operations for memory management.
I think an interesting project would be to implement a CIL interpreter on GraalVM.
DLR inspired the addition of invokedynamic opcode on the JVM, which is actually the foundation for how lambdas get generated, many still think nested anonymous classes are used.
GraalVM already handles LLVM bitcode, much cooler than plain MSIL.
And here there is another example where Java ecosystem ends up being better.
MSR had a compiler framework similar to GraalVM, called Phoenix, it was going to replace VS, LLVM style, instead it died and what is left are a couple of research papers.
The class gets generated to capture the `s` variable. Indy gets used in the `filter` method because the incoming lambda or method reference could be several types of method calls. For example, a constructor, an instance method, or a static method (I believe there are few more in the JVM bytecode).
What won't generate a new anonymous class is this sort of lambda
stream.filter(i->"foo".equals(i));
That will generate a new method on the parent class which ultimately gets called. Since nothing is captured it can be directly called without a new instance being created.
I was wrong, it looks like the case I gave is one which doesn't result in a new anonymous class being generated. Instead the lambda metafactory gets involved to avoid that allocation.
I apparently didn't see what I thought I saw. I thought that I had seen new `lambda.$1` classes being created in call stacks when debugging. Maybe I did, but the lambda was serializable (we have that in some unfortunate places in our code base).
There are still cases where new classes get generated, but that's pretty much just for serialization.
The ramifications for backwards compatibility is that the JVM won't have CLR features such as stackalloc (allocation of blocks of memory on stack), ref parameters (pass-by-reference of stack allocated value types), and all the other low-level/high-performance programming features available in the CLR.
Why couldn't it be done? Like sure, it won't happen in every case (e.g. too big value classes), but for a typical local variable of a value type, it's a trivial optimization to make it stack allocated. Even now it happens quite often (requires escape analysis), but the semantics change of value classes allows for it to be done "freely".
C# stackalloc returns a ‘ref struct’ which has certain restrictions and would be a Q-world type.
Java chose to go with an L-world implementation where everything is still a reference type on the heap, but memory management is more efficient via flattening. That’s why it’s a ‘value class’ and not a ‘struct’.
Primitive wrappers are scalarized into registers.
Stack allocated value types would have different semantics and is incompatible with the L-world implementation. Sure, they could implement it, but it would be the Q-world implementation that they decided to forego.
They can do automatic stack allocation for optimization via escape analysis as you mentioned. But, stackalloc is user allocated.
> Now, one can argue that this is just smoke and mirrors with type erasure and it is but you can already put a Date into a List<Point> if you're so inclined because the JVM doesn't know the difference, hence type erasure.
And that's a massive problem that they're planing to solve with specialized generics.
> If I read the correctly, it means that if you have a Point value class then on the JVM level you'll be able to stuff any value class into there if you're so incline, just like with List<Point>.
I'm not sure what you mean. An L-type is an object reference. E.g. Ljava/lang/String, Lcom/org/CustomObject. The issue you're conflating is the erasure of List<T> to List<Object> and it's L-type Ljava/util/List.
> I prefer the Hack version
Hack/HHVM didn't have to worry about backwards compatibility.
---
The blog post does a pretty high level overview of the implementation, but my understanding of it is the following:
1. They are adding a new bytecode class flag for value classes. Bytecode descriptor for value classes are exactly the same (L-type).
2. Primitive wrapper classes will become value classes.
The difference from the CLR is that the JVM implementation is backward compatible with linked legacy bytecode. As they can accept a value class instance into their methods due to the same L-type. It's just additional metadata added to allow the JVM to stack allocate the class.
---
Separately, to handle generics:
1. Parameterized container classes will be flagged with a new bytecode to enable parametric attributes on initialization with the additional data of the parameters in the Constant Pool. Bytecode descriptor for classes are exactly the same with type-erasure, e.g. List<SomeType> erased to Ljava/util/List.
2. Initialization of parameterized classes are done with the additional metadata of the type argument stored in the Constant Pool.
3. The runtime does monomorphization of the parameterized class.
It doesn't seem too different to what the CLR does, i.e. runtime monomorphization. The difference is the JVM implementation is backward compatible with older type-erased code, i.e. the restrictions in Java due to type-erasure are exactly the same as before and the L-types don't change. It's just additional metadata added to allow the JVM to monomorphize the classes for performance.
---
In summary, value objects and specialized generics are backwards compatible with legacy bytecode. The JVM handles the compatibility. Of course, the newer bytecode is not forward compatible with older JVMs.