I just don’t get how people are working that it represents a time cost rather than a large time savings. I don’t mean that as a dig, I just mean I genuinely don’t know what that must look like. And I’ve written a lot more code in dynamic languages, and got my start there, so it’s not like I “grew up” writing Java or something like that.
"I don't like to waste time writing tests, because I need that time to fix bugs on production that happened because I don't write tests".
The relation to static typing is that static types are a kind of test the computer automatically writes for you.
An example would be Common Lisp's `map` function [0] (it takes a number of sequences and a function that has as many parameters as there are sequences). It would be hard to come up with a type for this in Java, and it would be a pretty complicated type in Haskell.
Another example of many people's experience with static typing is the Go style of language, where you can't write any code that works for both a list of strings and a list of numbers. This is no longer common, but it used to be very common ~10-15 years ago and many may have not looked back.
[0] http://www.lispworks.com/documentation/HyperSpec/Body/f_map....
http://learnyouahaskell.com/functors-applicative-functors-an...
max <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
> [5,3,3,4]Anyway, bitter syntax sugar aside, the way you wrote the function I proposed was... a completely different function with similar results, which does not have the type I was asking for, and you only had to introduce 2 or 3 helper functions and one helper type to do it. I wanted to work with functions and lists, but now I get to learn about applicatives and ZipLists as well... no extra complication required!
Edit to ask: could this method be applied if you didn't know the number of lists and the function at compile time? CL's map would be the equivalent of a function that produces the expression you have showed me, but it's not clear to me that you could write this function in Haskell.
Edit2: found a paper explaining that this is not possible in Haskell, and showing how the problem is solved in Typed Scheme: https://www2.ccs.neu.edu/racket/pubs/esop09-sthf.pdf
> Another example of many people's experience with static typing is the Go style of language
Remember that a lot of backlash against Go's type system comes from static typing advocates used to more expressive static type systems :) It'd be a shame if, after all we complained about Go's limitations, newcomers held Go as an example of why static typing is a roadblock...
I mostly agree, don't get me wrong. And it's important to note that Common Lisp's `map` functions do more than what people traditionally associate with `map` - they basically do `map(foo, zip(zip(list1, list2), list3)...)`.
Still, this is a pretty useful property, and it is very natural and safe to use or implement, while being impossible to give a type to in most languages.
C++ can do it with the template system, as can Rust with macros (so, using dynamic typing at compile time).
Haskell can make it look pretty decent (if you can stand operator soup) by relying on auto-currying and inline operators and a few helper functions. I would also note that the Haskell creators also though that this functionality is useful, so they implemented some of the required boilerplate in the standard lib already.
In most languages, you can implement it with lambdas and zips (or reflection, of course).
So I think that this is a nice example of a function that is not invented out of thin air, is useful, is perfectly safe and static in principle, but nevertheless is impossible to write "directly" in most statically typed languages.
Just to show the full comparison, here is how using this would look in CL, Haskell and C#:
CL
(map 'list #'max3 '(1 3 5) '(-1 4 0) '(6 1 8))
Haskell
max3 <$> ZipList [1 3 5] <*> ZipList [-1,4,0] <*> ZipList [6,1,8]
OR
(<*>) ((<*>) ((<$>) max3 (ZipList [1,2])) (ZipList [-1,4])) (ZipList [3,1])
C#
new int[]{1,3,5}.Zip<int,int,Func<int,int>>(new int[]{-1,4,0}, (a,b) => (c) => max3(a, b, c)).Zip(new int[]{6, 1, 8}, (foo,c) => foo(c))
Note only the CL version, out of all these languages, can work for a function known at runtime instead of compile-time. None of the static type systems in common use can specify the type of this function, as they can't abstract over function arity.Here's a paper showing how this was handled in Typed Scheme: https://www2.ccs.neu.edu/racket/pubs/esop09-sthf.pdf
The language developers themselves have repeatedly stated that its type system being very limited is intentional.
See e.g. here: https://github.com/golang/go/issues/29649#issuecomment-45482...
TBH, sometimes I wonder why they bothered with static typing at all...
This is not a question of just supporting parametric polymorphism, but of abstracting over the number of arguments of a function, which is not supported in almost any type system I know of; and then of matching the number of arguments received with the type of function you specified initially.
self[expr.type](self, expr)There are better languages for expressing this more natural (such as Idris) but in the end, the fallacy seems to lie in your claim that this would be "safe and easy to do with dynamic typing". That's what you think until you find out that your solution works in 99% of the cases, except in some special cases, because the compiler didn't have your back.
Examples are the standard sort functions in Java and python, which were bugged for a very long time.
Btw, here is the executable code in Scala: https://scalafiddle.io/sf/UrDu12b/1
Posting it for reference in case Scalafiddle is down:
import shapeless._, ops.function._
def multiMap[InputTypes <: HList, MapF, HListF, MapResult] (inputs: List[InputTypes])(mapF: MapF)
(implicit fn: FnToProduct.Aux[MapF, InputTypes => MapResult]) = inputs.map(fn(mapF))
val testList2Elems = List(
3 :: "hi" :: HNil,
5 :: "yes" :: HNil
)
multiMap(testList2Elems){ (num: Int, str: String) =>
s"$num times $str is ${List.fill(num)(str).mkString}"
}.foreach(println)
val testList3Elems = List(
3 :: "hi" :: 3 :: HNil,
5 :: "yes" :: 2 :: HNil,
2 :: "easy" :: 1 :: HNil
)
multiMap(testList3Elems){ (num: Int, str: String, mult: Int) =>
s"$num * $mult times $str is ${List.fill(num*mult)(str).mkString}"
}.foreach(println)
// As expected, the compiler has our back and the following does not compile
val testListWrongElems = List(
3 :: "hi" :: HNil,
5 :: "yes" :: "ups?" :: HNil
)
/*
* Whoops, does not compile, list shape not good for multiMap :)
*
multiMap(testListWrongElems){ (num: Int, str: String) =>
s"$num times $str is ${List.fill(num)(str).mkString}"
}.foreach(println)
*/
/*
* Whoops, does not compile, 2-sequences vs 3 argument function :)
*
multiMap(testList2Elems){ (num: Int, str: String, mult: Int) =>
s"$num * $mult times $str is ${List.fill(num*mult)(str).mkString}"
}.foreach(println)
*/template <typename... Lists, typename Func> auto map(Func && func, Lists &&... lists) -> std::vector<decltype(func(std::declval<typename std::decay<Lists>::type::value_type>()...))>;
Yes, nothing hard to understand or discover about that at all...
Is this an unusual use case?
This is the classic use case for code generation. (And IMO one of the few justified ones.)
1. There's no guarantee the correct theoretical model of your program fits the type system of your programming language.
2. Sometimes there are multiple correct models for different purposes in the same program, similar to how sometimes you need multiple views onto the same database tables.
3. Sometimes you just need the ability to bodge things.
Just wanted to point out that even though you can have multiple views or your database tables, they all still adhere to the same type system.