Can haskell correctly model models? I'm have very little experience with Haskell. I have never seen a common imperative oop based programming language that can use models.
For those uninitiated, a model is basically a collection of types. It is a category where the objects are types. A Natural transform is then used to model a model.
Quite simply:
class mB;
class mD : mB { mD foo(); };
Our model then is {mB, mD} and all the relationships in them(the objects and morphism).
We want to derive a new model from the original that parallels is completely,
class MB;
class MD : MB { MD foo(); };
This new model is {MB, MD} and should parallel all the functionality, it is essentially a copy and pasted version, so to speak.
There is no relationship, but we can use inheritance to act as a functor:
class MB : mB
class MD : mD, MB { MD foo(); };
So, M can substitute for m, every type in the model M is derived from a type in the model M. It is oop at a higher level of abstraction. We effectively are trying to define inheritance for models, not just standard classes.
The problem, though, is that M.foo does not override m.foo. Variance is broken!
This is not a problem for models though because We never will mix and match models(but it does require proper construction).
This might be unclear for some but it is quite easy. A natural transformation exists due to the inheritance:
mB -> MB
| |
v v
mD -> MD
One can easily think of this in more concrete terms.
Suppose one has a Model F16 which is a real F16 fighter jet.
One now wants to create a 1:16 toy model version(or vice versa). Every component will be modeled and so there is a correlation between the two models. The real components and the toy components.
If the real F16 forms one category then the toy will form another, but there will be natural relationships.
What we want to avoid is connecting components between models in an natural way. E.g., using a real jet engine in the toy model or vice versa.
Since Haskell is based in category theory I imagine it can properly handle this but I don't know how or how effective.
Essentially we need to take a model(which is a category) and be able to "duplicate it" but still be able to substitute the new model in to the original(covariance/inheritance).
The new model, since everything inherits, will function as a substitute for the old one. The new model can only extend(because it's covariantly derived).
There are a few issues because of the complexity but the idea is that that we are taking one model and producing a new derived model, not really any different than when we take one class and produce a new derived class. The derived class can be substituted for the original because it contains the original in all structure(inheritance is a natural transformation).
Here is another way to think about it, and what I'd like to use it for:
Create the business model, no structure related to anything but the pure business end of the model. Maybe it is a checking account.
Extend the model(the entire model) to have a GUI. The GUI uses the business model in one but the business model has no idea of the GUI.
Typically this is done using a modelViewModel type of thing and is working at the model level(we have two models, the base model and the derived model).
Usually though it is somewhat of a mess for large complex objects. It should be symbol.
model M : m;
i.e., just like classes, we create a new model M that extends m, we then add the new structure but all the base structure is preserved. The thing is, in M, there is no direct assess of stuff in m. i.e., we can violate variance in some sense because we know the base model is preserved in it's entirely and there is a 1:1 relationship between the two. i.e., the example above with foo is valid and foo can return a contravariant object and still override the original base models type. This normally is bad but in this case MD.foo is only ever used in the model M (when the base model uses foo, it's always using it on objects from M).
One can almost think of it like taking a complex model and simply copying and pasting all the code then renaming all the types in a consistent fashion, then only adding new structure. The model then contains the structure of the original and can be substituted for it.
The idea here is to maximize separation of code... to reduce all dependencies to the minimal required.. but have the compiler do all the plumbing so we don't have to worry about making mistakes or creating unintentional dependencies across the two models.
Some D source code which can be plugged in to https://run.dlang.io/
import std.stdio, std.traits;
struct ModelA
{
// D only allows single inheritance, must use interfaces
interface iAnimal
{
string Type();
string Name();
void Attack(iAnimal who);
iFood LikesWhichFood();
}
interface iCat : iAnimal
{
void Meow();
}
interface iDog : iAnimal
{
void Bark();
}
interface iFood
{
}
class Animal : iAnimal
{
void Attack(iAnimal who) { writeln(Name, " is attacking ", who.Name, "!"); }
string Type() { return "Unknown Animal Type"; }
override string Name() { return "Unknown Animal"; }
iFood LikesWhichFood() { writeln("Food D Type: ", fullyQualifiedName!iFood); return null; }
}
class Cat : Animal, iCat
{
string name = "Unknown Cat";
override string Type() { return "Cat"; }
override string Name() { return name; }
void Meow() { writeln("Meow!"); }
this() { }
this(string n) { name = n; }
}
class Dog : Animal, iDog
{
string name = "Unknown Dog";
override string Type() { return "Dog"; }
override string Name() { return name; }
void Bark() { writeln("Bark!"); }
this() { }
this(string n) { name = n; }
}
class Food : iFood
{
}
}
// Model B, It is "derived" from A, meaning Model B could, in theory, substitute for Model A as long as everything is designed correctly
// In this case we will create a ViewModel, a gui framework for ModelA. We actually cannot do this naturally in D since it does not support multiple inheritance.
struct ModelB
{
interface iAnimal : ModelA.iAnimal
{
override iFood LikesWhichFood();
}
interface iCat : iAnimal, ModelA.iAnimal
{
}
interface iDog : iAnimal, ModelA.iAnimal
{
}
interface iFood : ModelA.iFood
{
void IsItTasty();
}
class Animal : ModelA.Animal, iAnimal
{
//
override iFood LikesWhichFood() { return cast(iFood)super.LikesWhichFood; }
}
class Cat : ModelA.Cat, iAnimal, iCat // We need to derive from Animal, not iAnimal, to provide proper ModelB implementation of Animal
{
alias Attack = Animal.Attack; // Required by D
// In D, ModelA.Cat's implement is not provided as default, we have to reimplement everything. Or is Animal providing any implementation
override string Type() { return super.Type; }
override string Name() { return super.Name; }
override void Meow() { super.Meow; }
void Attack(iAnimal who) { super.Attack(who); }
override void Attack(ModelA.iAnimal who) { super.Attack(who); }
override iFood LikesWhichFood() { writeln("Food D Type: ", fullyQualifiedName!iFood); return new Cabbage; }
this() { }
this(string n) { name = n; }
}
class Dog : ModelA.Dog, iAnimal, iDog
{
alias Attack = Animal.Attack;
override string Type() { return super.Type; }
override string Name() { return super.Name; }
override void Bark() { super.Bark; }
void Attack(iAnimal who) { super.Attack(who); }
override void Attack(ModelA.iAnimal who) { super.Attack(who); }
override iFood LikesWhichFood() { writeln("Food D Type: ", fullyQualifiedName!iFood); return new Donuts; }
this() { }
this(string n) { name = n; }
}
class Food : iFood
{
void IsItTasty() { writeln("Unknown Food"); }
}
class Donuts : Food
{
override void IsItTasty() { writeln("YUK!"); }
}
class Cabbage : Food
{
override void IsItTasty() { writeln("YUM!"); }
}
}
void main()
{
{
ModelA.iAnimal animal1 = new ModelA.Cat("Mittens");
ModelA.iAnimal animal2 = new ModelA.Dog("Sparky");
writeln(animal1.Name);
writeln(animal2.Name);
animal1.Attack(animal2);
animal1.LikesWhichFood;
}
writeln("\n----------\n");
{
ModelB.iAnimal animal1 = new ModelB.Cat("Super Mittens");
ModelB.iAnimal animal2 = new ModelB.Dog("Super Sparky");
writeln(animal1.Name);
writeln(animal2.Name);
animal1.Attack(animal2);
auto f = animal1.LikesWhichFood;
//f.IsItTasty; // Error: no property `IsItTasty` for type `Models.ModelA.iFood`. It should return a ModelB.iFood, we are inside ModelB, never any risk
(cast(ModelB.iFood)f).IsItTasty; // We can, of course, force it, but that is the rub, we don't have to, that is why we want to have a concept of a model, it tells the compiler that there is something more going on and it can reduce all this overhead. We can't even override this because of the contravariance rule.
}
writeln("\n----------\n");
// This is the magic, ModelB is now substituted in Model A. It's basically still oop but our entire derived model is(or should be) used.
// We can substitute the new model in all places where the old was used. This is the easy way to do ModelViewModel, we simply extend the model and add the view, no complex bridging, adapting, maintance, dependencies, etc.
{
ModelA.iAnimal animal1 = new ModelB.Cat("Super Mittens");
ModelA.iAnimal animal2 = new ModelB.Dog("Super Sparky");
writeln(animal1.Name);
writeln(animal2.Name);
animal1.Attack(animal2);
animal1.LikesWhichFood;
auto f = animal2.LikesWhichFood;
//f.IsItTasty; // This Error is ok, we are inside ModelA, ModelA would never use IsItTasty and it would be wrong to do so(it's only wrong because it should be impossible for ModelA to know about ModelB, else we create a dependency between models and really end up with one combined model rather than two separate models). But note that we could cast
(cast(ModelB.iFood)f).IsItTasty; // We can, of course, force it though(only because we know for a fact we are actually dealing with a ModelB disugised as a ModelA, this is generally not the case), but this then shows a dependency. Note that it is exactly like the above model though... but there is a huge difference. In the first case it is afe, in this case it is not.. and the only difference is the model we are working in.
}
}
The thing to note here is that there are some issues with D, we have quite a bit of verbosity. We must add many methods just to keep the model consistent methods. Ideally every class and interface in ModelB could be empty and things would compile. In fact, ideally we could do something like
model ModelB : ModelA {
}
and provide just what we want to extend or modify. (just as we would do for a base and derived class. A model is essentially a slightly more abstract class where the members are classes.)
