This is a mistaken view point. Every function you can ever write is bounded by a type, thus whether you use Ruby, PHP, or Rust can be described by a bounded type.
Every time you write logic in your code, your brain is aware that the logic is dealing with a specific type. Writing a type signature on top of that is just additional instructions to the compiler about the type you have already specified in your logic. It is a minor inconvenience.
There are many reasons why Ruby and PHP won out. One of the reasons is many people misunderstand the power and flexibility of types. Once they understand this, they will know there is really no trade off between statically typed and dynamically typed. Statically typed languages are infinitely better and the only downside is a minor inconvenience.
First off, note that there is only one function in the universe that can really take every single type in existence and that function is shown below:
-- haskell
identity :: x -> x
identity x = x
# python
def identity(x: Any) -> Any:
return x
This is the only untyped function in existence. Every other function in the universe must be typed whether it is typed dynamically or statically is irrelevant, either way your code will have to specify the type in logic or in the type signature. You can see this happening in the examples below...
The above example have no logic to specify a type... The minute you start to add any logic to your function immediately your function becomes bounded by a type. Let's say I simply want to add one in my identity function.
-- haskell
identity :: Int -> Int
identity x + 1 = x + 1
# python
def identity(x: Int) -> Int:
return x + 1
The very act of adding even the simplest logic binds your function to a type. In this case it binded my function parameter to an integer. Whether you use PHP or Ruby or Rust there is a type signature that describes all functions.
Let's say I want to do garbage code like write a function that handles an Int or a String? That can be typed as well....
--haskell
type IntOrString = Number Int | Characters String
func :: IntOrString -> IntOrString
func Number n = n + 1
func Characters n = n ++ "1"
#python
IntOrString = Union[str, int]
def func(n: IntOrString) -> IntOrString:
if isinstance(n, int):
return n + 1
if isinstance(n, str):
return n + "1"
Let's say I want to handle every possible JSON api in existence? Well it can be typed as well.
-- haskell
type JSON = Number Float | Characters String | Array [JSON] | Map String JSON
func :: JSON -> JSON
func Array x = x ++ [1.0]
func x = x
-- python
JSON = Union[float, str, List["JSON"], Dict[str, "JSON"]]
def func(x: JSON) -> JSON:
if isinstance(x, list):
return x + [1.0]
else:
return x
There really isn't any additional flexibility afforded to you by a dynamically typed language other than the slight overhead of translating the types you already specified dynamically into types that are specified statically.
One caveat to note here (and this is specific to haskell) is lack of interfaces specific to record types. I cannot specify a type that represents every single record that contains at least a property named x with a type of int.