I'm honestly not convinced it helps that much. And it seems to cost a lot to me.
I like database and API schemas though. And I like clojure.spec and function preconditions a lot.
I'm honestly not convinced it helps that much. And it seems to cost a lot to me.
I like database and API schemas though. And I like clojure.spec and function preconditions a lot.
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.
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.
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....
self[expr.type](self, expr)> 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
http://learnyouahaskell.com/functors-applicative-functors-an...
max <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
> [5,3,3,4]More explicitly, the expression there seems to rely on knowing the arity of the function and the number of lists at compile time. Basically, I was asking for a function cl_map such that:
cl_map foo [xs:[ys:[zs:...]]] = foo <$> xs <*> ys <*> zs <*> ...
Edit: 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 > traverse_ print (sequenceA [ZipList [1,2], ZipList [3,4]])
[1,3]
[2,4]Basically, instead of your example I would like to do something like this:
> cl_map (+) [ZipList [1,2,3], ZipList [4,5,6]]
[5,7,9]
> cl_map (+ 3) [ZipList [1,2,3]]
[4,5,6]
> cl_map max3 [ZipList [1,2], ZipList [3,4], ZipList [5,6]] where max3 x y z = max x (max y z)
[5, 6]
Can this be done? What is the type of cl_map?Note: If this doesn't work with ZipList, that's ok - the important part is being able to supply the function at runtime. Also, please don't assume that the function is associative or anything like that - it's an arbitrary function of N parameters.
> (+) <$> ZipList [1,2,3] <*> ZipList [4,5,6]
ZipList {getZipList = [5,7,9]}
> (+3) <$> ZipList [1,2,3]
ZipList {getZipList = [4,5,6]}
> let max3 x y z = max x (max y z)
> max3 <$> ZipList [1,2] <*> ZipList [3,4] <*> ZipList [5,6]
ZipList {getZipList = [5,6]}
If you want to use "functions unknown at runtime that could take
any number of arguments" then you'll have to pass the arguments
in a list. Of course these can crash at runtime, which
Haskellers wouldn't be happy with given an alternative, but
hey-ho, let's see where we get. > let unsafePlus [x, y] = x + y
> fmap unsafePlus (sequenceA [ZipList [1,2,3], ZipList [4,5,6]])
ZipList {getZipList = [5,7,9]}
> let unsafePlus3 [x] = x + 3
> fmap unsafePlus3 (sequenceA [ZipList [1,2,3]])
ZipList {getZipList = [4,5,6]}
> unsafeMax3 [x, y, z] = x `max` y `max` z
> fmap unsafeMax3 (sequenceA [ZipList [1,2], ZipList [3,4], ZipList [5,6]])
ZipList {getZipList = [5,6]}
So the answer to your question is that cl_map :: ([a] -> b) -> [ZipList a] -> ZipList b
cl_map f = fmap f . sequenceA
except you don't actually want all the elements of the list to be
of the same type, you want them to be of dynamic type, so let's
just make them Dynamic. > let unwrap x = fromDyn x (error "Type error")
>
> let unsafeGreeting [name, authorized] =
> if unwrap authorized then "Welcome, " ++ unwrap name
> else "UNAUTHORIZED!"
>
> fmap unsafeGreeting (sequenceA [ZipList [toDyn "tome", toDyn "simiones", toDyn "pg"]
> , ZipList [toDyn True, toDyn True, toDyn False]])
ZipList {getZipList = ["Welcome, tome","Welcome, simiones","UNAUTHORIZED!"]}
and the type of cl_map becomes cl_map :: ([Dynamic] -> b) -> [ZipList Dynamic] -> ZipList b
cl_map f = fmap f . sequenceA
One could polish this up a bit and make a coherent ecosystem out
of it, but Haskell programmers hardly ever use Dynamic. We just
don't come across the situations where Clojurists seem to think
it's necessary.It's nice that Haskell does offer a way to circumvent the type system to write somewhat dynamic code, but it's a shame that in order to write a relatively simple function we need to resort to that.
Note that the type of cl_map is perfectly static. It would be `Integer N => (a_0 ->... a_N -> r) -> [a_0] ->... [a_N] -> [r]` assuming some fictitious syntax.
Steady on! You posed a question and I gave an answer. You weren't happy with that answer. I think it's a bit premature to conclude that "this function can't be written in a simple, safe way in Haskell".
> as the article I linked claims, Haskell's type system can't encode the type of the cl_map function.
Could you say where you see that claim in the article? I can see three mentions of "Haskell" in the body, two of them mentioning that one researcher's particular implementation doesn't handle this case, but not a claim that it can't be done.
> Note that the type of cl_map is perfectly static. It would be `Integer N => (a_0 ->... a_N -> r) -> [a_0] ->... [a_N] -> [r]` assuming some fictitious syntax.
OK, fine, it's a bit clearer now what you are looking for. How about this:
> cl_map (uncurry (+)) ([1,2,3], [4,5,6])
[5,7,9]
> cl_map (+3) [1,2,3]
[4,5,6]
> let max3 (x, y, z) = x `max` y `max` z
> cl_map max3 ([1,2], [3,4], [5,6])
[5,6]
Notice that the function arguments are have different,
statically-known types! The type of this miracle function? cl_map :: Default Zipper a b => (b -> r) -> a -> [r]
And the implementation? -- Type definition
newtype Zipper a b = Zipper { unZipper :: a -> ZipList b } deriving Functor
-- Instance definition
instance a ~ b => D.Default Zipper [a] b where def = Zipper ZipList
-- These three instances are in principle derivable
instance P.Profunctor Zipper where
dimap f g = Zipper . P.dimap f (fmap g) . unZipper
instance Applicative (Zipper a) where
pure = Zipper . pure . pure
f <*> x = Zipper (liftA2 (<*>) (unZipper f) (unZipper x))
instance PP.ProductProfunctor Zipper where
purePP = pure
(****) = (<*>)
Given that the only two lines that actually matter are newtype Zipper a b = Zipper { unZipper :: a -> ZipList b } deriving Functor
instance a ~ b => D.Default Zipper [a] b where def = Zipper ZipList
and the rest are boiler plate that could be auto-derived, I think this
is pretty satisfactory. What do you think?Still, you haven't written exactly the function I was asking for. You require a manual, compile-time step of transforming the N-ary function to a unary function taking a tuple. Still, it's impressive that this can define variable-length, variable-type tuples. Unfortunately I am not able at all to follow your solution, as it's using too many types that I'm not familiar with, and it seems to require some external packages, so I can't easily try it out in an online compiler to understand it better (as I have been doing so far).
Either way, I would say we are well outside the limits of an easy to understand way of specifying this kind of function - even if you are only showing 2 lines of code, it seems that your definition requires, outside of lists and functions (the objects we intended to work with): ZipList, Default, Functor, Profunctor, ProductProfunctor, Applicative, and a helper type. Even if these were derivable, someone seeking to write this function would still need to be aware of all of these types, some of which are not even part of the standard library; and of the way they work together to magically produce the relatively simple task they had set out to do.
> Could you say where you see that claim in the article?
The claim is presented implicitly: for one, they conjecture that, were Haskell or SML to "pragmatically support" such a feature, it would be used more often (offering as argument the observation that both Haskell's and SML's standard libraries define functions that differ only in the arity of their arguments, such as zipWith/zipWith3 in Haskell). This implies that, to their knowledge, it is not pragmatically possible to implement this in Haskell.
Similarly, given that in their "Related Works" section they don't identify any complete implementation of variadic polymorphism, it can be assumed that they claim at least not to have found one.
Try this. It's a full working program. The packages it depends on are "profunctors" and "product-profunctors".
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
module MyExample where
import qualified Data.Profunctor as P
import qualified Data.Profunctor.Product as PP
import qualified Data.Profunctor.Product.Default as D
newtype Zipper a b = Zipper { unZipper :: Traverse ZipList a b }
deriving Functor
instance a ~ b => D.Default Zipper [a] b where
def = Zipper (P.dimap ZipList id D.def)
instance P.Profunctor Zipper where
dimap f g = Zipper . P.dimap f g . unZipper
instance Applicative (Zipper a) where
pure = Zipper . pure
f <*> x = Zipper ((<*>) (unZipper f) (unZipper x))
instance PP.ProductProfunctor Zipper where
purePP = pure
(****) = (<*>)
cl_map :: D.Default Zipper a b => (b -> r) -> a -> [r]
cl_map f = getZipList . fmap f . runTraverse (unZipper D.def)I'm afraid I'm now completely stumped about what you're asking for. If you have a function with a known arity and want to apply it to a known number of arguments then you can use the original formulation:
f <$> args1 <*> args 2 ... <*> argsN
You then asked what happens for unknown numbers of arguments, so I produced a solution that works with lists, which isn't very Haskelly, but does the job. After that you said you wanted something with a more specific type, so I came up with the answer that works generally over tuples (or indeed any type that contains a sequence of arguments). That's not satisfactory either! It seems you literally want a function with type `Integer N => (a_0 ->... a_N -> r) -> [a_0] ->... [a_N] -> [r]`. Well, I don't know how to do that in Haskell -- maybe my most recent solution extends to that -- but nor do I know why you'd want to do that! If you have a known number of arguments the first solution works fine. If you have an unknown number of arguments then you must have them all together in one datastructure, so the most recent solution works fine. Haskellers would be very happy with either of those and I don't see how we're missing out on programming convenience because of that. Maybe you could elaborate?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
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.
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...
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)
*/"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.
Is this an unusual use case?
This is the classic use case for code generation. (And IMO one of the few justified ones.)
On the other end people who prefer weakly typed languages see problems as primarily that of data transformation. For example from HTML Form to Http Request to SQL Db to CSV File and so on.
Both approaches are differentiated by the perspective on the problem.
Static/dynamic is whether type checking is done at compile time or run type.
Strong/weak is how flexible the language is with type conversion.
Another explanation: https://en.hexlet.io/courses/intro_to_programming/lessons/ty...
Strongly typed dynamic: Python
Weakly typed dynamic: Javascript
Strongly typed static: Haskell
Weakly typed static: C
As in:
Static typing - 1 == "1" is false (Python style, integer isn't converted to string for comparison)
Dynamic typing - 1 == "1" is true (PHP style, integer or string may be converted)
Liskov, B; Zilles, S (1974). "Programming with abstract data types".
http://web.cs.iastate.edu/~hridesh/teaching/362/07/01/papers...
That, plus type inference makes the static typing pretty painless.
These data types are much more powerful than the fancy arrays that wowed me back in the day :)
There's also the case that I find the type systems of Rust, Elm, etc to be much more helpful than the type systems of C++ or Sorbet (type system for Ruby).
But my current job has very, very little that would benefit from static typing. Adding it into the mix would slow us down, both literally and figuratively.
Would you mind expanding on this? I'd be interested in what processes you have and if you use any additional tooling.
Thanks!
For database stuff, an ORM with some validation rules is generally enough, and couldn't be replaced with static typing anyhow.
For anything that absolutely has to be a certain kind of data, there are things built into dynamic languages to check the type of something, and you just call it as needed on a case-by-case basis.