I find naming types to be a major pain. In complex projects, you often end up with multiple types that are almost the same but have a few attributes different. In reality these are the same type, it's just not supposed to have a single fixed schema.
For example, how would you model a tadpole which develops into a frog with a statically typed language? Do you need two different types for this? What about all the awkward intermediate states between tadpole and frog? How do you model those? Do you really need to create multiple different instances in the code to represent that one entity?
Dynamically typed languages can be much quicker and cheaper to write, making for more rapid business product cycles. The lack of type safety can largely be made up by adding to the already necessary unit tests.
Isn't that how the YouTube startup out-manoeuvred Google's much larger dev team working with static types?
Code isn't just written, though. It's read, modified, and maintained. Sure, you can write it faster. But are you building that program for this week, or for the next two decades?
(Yes, I know, you can be in a situation where writing it faster now is life-or-death for the company. Most of us won't spend most of our careers in that situation, though.)
Only it very much matters...
And to track it you have an ad-hoc, incomplete, custom, buggy, implementation of type checking in your head -- as opposed to in the program, and statically checked.
>Maybe it's a new kind of type that you could not have imagined when you first wrote the code, but if it supports the necessary method, it's going to work and you don't need to change or recompile your code to account for it.
You obviously need to change your code to have objects of that "new kind of type" (even if they are dynamic and untyped). Where would the new objects come from without changing the code in some place, the ether?
Supporting the necessary method is a very poor check for "it's going to work".
A "bomb" and a "lock" object might both have an "activate" method. You wouldn't want to call the former when you meant the latter.
You're still doing the same checks, only know you do only half of them, in your head, and whatever goes...
let frob something = something#run ...
and it will enforce the fact that whatever is passed to frob, it must have a method named "run", and it must have the requisite number of arguments and their types.The types are still there, though. They're just types like, "some object with method "run" with ...".
If the state is modeled in numeric terms then you can represent the intermediate state as a range of integers. You need to do this whether or not your program is typed.
Without types you're allowing the program to fall into a state that it doesn't know how to handle. This works because the program doesn't try to handle these intermediate invalid states. What happens when it eventually does try to handle that state? That's when bugs happen.
Static type systems are a brute force way to eliminate a certain class of bug. A dynamic language appears simpler because it doesn't even attempt to capture those errors. It's like saying your payment validation is "simpler" because it doesn't do CCV checks.
// Tadpole state, can swim but cannot walk.
return {
color: 'black',
size: 10,
speed: 5,
direction: 3.14,
swim: function () {
// ...
}
};
}function allowFrogToWalk(frog) {
frog.walk = function () {
// ...
};
}function allowFrogToJump(frog) {
frog.jump = function () {
// ...
};
}function makeFrogToxic(frog) {
frog.intoxicate = function (animal) {
// ...
};
}let frog = createFrog();
// ...
// Then when the frog reaches a certain size: allowFrogToWalk(frog);
// After the frog spends a certain amount of time on land.
allowFrogToJump(frog);
// If frog eats a certain toxic plant.
makeFrogToxic(frog);
// ...
// Let's say our game has many different kinds of animals and we manage them
// from a single place in the code (and they can interact with each other).
// When an animal tries to go on land:
if (animal.walk) {
// ...
}// If animal enounters an obstacle:
if (animal.jump) {
// ...
}// When an animal tries to go on water:
if (animal.swim) {
// ...
}// When two animals touch each other
if (touches(animalA, animalB)) {
if (animalA.intoxicate) {
animalA.intoxicate(animalB);
}
// ...
}Doing this with a statically typed language would add a lot of unnecessary complexity. You'd need to create a lot of types/interfaces:
- Animal (which has all the generic properties of an animal)
- WaterAnimal (which inherits from Animal and can swim but not walk)
- LandAnimal (which inherits from Animal and can walk but not swim)
- Tadpole (inherits from WaterAnimal)
- ToxicTadpole (inherits from tadpole but adds intoxicate method)
- YoungFrog (inherits from WaterAnimal and LandAnimal and is constructed from a Tadpole instance, cannot jump)
- ToxicYoungFrog (inherits from YoungFrog but adds intoxicate method)
- MatureFrog (inherits from WaterAnimal and LandAnimal and is constructed from a YoungFrog instance, has the ability to Jump)
- ToxicMatureFrog (inherits from MatureFrog but adds intoxicate method)
As you add more complex functionality, you either have to support more permutations of different types or you have come up with some kind of complex decorator pattern which subverts the whole purpose of having static types to begin with.
I also have to notice that this is not a very typical problem. I don't seem to remember that I have ever had to model something like this in a programming language.
"Don't do that then"
This kind of problem with using heavily inheritance-based systems for behavior can be addressed by using entity component systems. Often found in game development. The general guidance these days is to use "is-a" relationships less and "has-a" relationships more.
The point that I'm trying to make is that true compile-time type safety is not possible in my example without some kind of rigid complex inheritance structure that is predefined at compile time.
type walker = WalkFn of (animal -> unit)
and jumper = JumpFn of (animal -> unit)
and swimmer = SwimFn of (animal -> unit)
and intoxicator = ToxicFn of (animal -> animal -> unit)
and animal = Animal of animal_data
and animal_data = {
name: string;
color: string;
mutable walk: walker option;
mutable jump: jumper option;
mutable swim: swimmer option;
mutable intoxicate: intoxicator option
}
let can_walk = function
| Animal { walk = Some _ } -> true
| _ -> false
let walk animal =
match animal with
| Animal { walk = Some (WalkFn walk_fn) } -> walk_fn animal
| _ -> invalid_arg "this animal can't walk"
let default_walker = function
| Animal { name; color } -> Printf.printf "the %s %s walks\n" color name
let allow_to_walk (Animal data) =
data.walk <- Some (WalkFn default_walker)
(* --- more of the same for the other actions --- *)
let can_jump = function
| Animal { jump = Some _ } -> true
| _ -> false
let jump animal =
match animal with
| Animal { jump = Some (JumpFn jump_fn) } -> jump_fn animal
| _ -> invalid_arg "this animal can't jump"
let default_jumper = function
| Animal { name; color } -> Printf.printf "the %s %s jumps\n" color name
let allow_to_jump (Animal data) =
data.jump <- Some (JumpFn default_jumper)
let can_swim = function
| Animal { swim = Some _ } -> true
| _ -> false
let swim animal =
match animal with
| Animal { swim = Some (SwimFn swim_fn) } -> swim_fn animal
| _ -> invalid_arg "this animal can't swim"
let default_swimmer = function
| Animal { name; color } -> Printf.printf "the %s %s swims\n" color name
let allow_to_swim (Animal data) =
data.swim <- Some (SwimFn default_swimmer)
let can_intoxicate = function
| Animal { intoxicate = Some _ } -> true
| _ -> false
let intoxicate animal victim =
match animal with
| Animal { intoxicate = Some (ToxicFn toxic_fn) } -> toxic_fn animal victim
| _ -> invalid_arg "this animal can't intoxicate"
let default_intoxicator animal victim =
match animal, victim with
| Animal attacker, Animal victim ->
Printf.printf "the %s %s poisons the %s %s\n"
attacker.color attacker.name victim.color victim.name
let allow_to_intoxicate (Animal data) =
data.intoxicate <- Some (ToxicFn default_intoxicator)
(* --- end of actions --- *)
let create_frog () =
Animal {
name = "frog";
color = "black";
walk = None;
jump = None;
swim = None;
intoxicate = None
}
let create_cow () =
Animal {
name = "cow";
color = "green";
walk = None;
jump = None;
swim = None;
intoxicate = None
}
let story () =
let frog = create_frog () in
Printf.printf "newborn frog, can it walk? %s\n"
(if can_walk frog then "yes" else "no");
allow_to_walk frog;
Printf.printf "and now, can it walk? %s\n"
(if can_walk frog then "yes" else "no");
walk frog;
allow_to_swim frog;
swim frog;
allow_to_jump frog;
jump frog;
allow_to_intoxicate frog;
intoxicate frog (create_cow ());
Printf.printf "the end\n"
(* now run all this *)
let _ = story ()
And here's what it looks like when it's run: $ ocaml frog_story.ml
newborn frog, can it walk? no
and now, can it walk? yes
the black frog walks
the black frog swims
the black frog jumps
the black frog poisons the green cow
the end
It's true that the animal type must have types and fields for all possible actions an animal might ever take. It would be possible to compartmentalize things a bit more, maybe. But for a quick prototype this should work quite well, I think. let create_frog () = object
method color = "black"
method size = 10
method speed = 5
method direction = 3.14
method swim = print_endline "Frog swimming"
end
let allow_frog_to_walk frog = object
method walk = print_endline "Frog walking"
method super = frog
end
let allow_frog_to_jump frog = object
method jump = print_endline "Frog jumping"
method super = frog
end
let make_frog_toxic frog = object
method intoxicate animal = print_endline ("Intoxicate" ^ animal)
method super = frog
end
let frog = create_frog ()
let frog = allow_frog_to_walk frog
let frog = allow_frog_to_jump frog
let frog = make_frog_toxic frog