Motivation – Keli Language
keli-language.gitbook.io
keli-language.gitbook.io
The other reason is that many FP users are too enthusiastic about creating abstractions. This is of course something that FP is exceptionally well suited for. An api that was written to simply process a list of Orders into a Report might be abstracted into a fold on some monad, which at first seems a great idea. But if you're not careful, readability suffers a lot. It's much easier to get to know an application when its code deals with business objects that you already understand well, than to read hundreds of lines of code that deal only with abstractions.
Maybe there should be a new edition of Clean Code written specifically for FP, to address this anti pattern.
By the way this problem is not unique for FP, OOP suffered the same problem in the past. But the OOP community learned from its mistakes and now most developers know to 'prefer composition over inheritance', for instance.
Do you have a code example? It would be interesting to see what this kind of code looks like.
Personally, I find complaining about a ”fold over a monad” to be equivalent to complaining about an ”integer indexed loop over an array”. It’s a pretty straightforward implementation detail that have very little to do with your business logic.
And business logic is far, far easier to model using ADTs (it’s literally just AND and OR, applied to data structures) than confounding object inheritance hierarchies.
In fact, I’ve seen a lot more unnecessarily abstracted garbage (usually “design pattern” workarounds to limitations in the object model) in just about every oop based web framework I’ve worked with.
https://docs.spring.io/spring-framework/docs/current/javadoc...
https://docs.spring.io/spring-framework/docs/current/javadoc...
I think I still have a copy of the book that Johnson wrote to accompany the creation of Spring (which someone wants $300 bucks for on Amazon, I didn't know it was a collector's item). The last time I used Spring, I had the distinct thought that I should go through his book as if it were a bingo card, comparing it with the latest version. It seems like it has reacquired all of the bits of J2EE that it set out to avoid.
If, outside of Spring, there is anything in the Java world that more perfectly embodies Nietzsche's warning about fighting with monsters, I don't know what it is.
This comes across as an old fashioned view (bordering on extremely old-fashioned).
int id;
public int Id
{
get {
return id;
}
set(val) {
id = val;
}
}
They're now one liners, and obviously necessary public int Id { get; set; }
I don't keep up with Java any more, but C# has super short shortcuts for assignment like: public int Id { get; set; } => generateId();
If you, or your company, are misusing a completely optional documentation feature, which you definitely should not be using to annote obvious stuff like IDs, that's your problem, not OOPs.Switching to a functional language won't save your monitor, you'll still be forced to write bad code if doing stupid things like that are in your style guide.
private int id;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}I honestly believe we're in a dire need for a more up-to-date metaphor that lets us better express our anger at a computer, due to monitors becoming paper thin and lightweight in the past decade or so.
> I honestly believe we're in a dire need for a more up-to-date metaphor that lets us express our anger at a computer, due to monitors becoming paper thin and lightweight in the past decade or so.
My experience may not be representative, but I believe I have encountered variants of "hurl the keyboard at the monitor" more often than "monitor out the window".
OTOH, that phrase doesn't account for laptops.
It is also not anger-ready for people working on tablets with rubber keyboards attached.
> As more operations are demanded, it is more likely that the existing partitioning of the world into the nice distinct cases won’t suit that operation anymore. Then, as a fix, we can introduce more specific cases, or make existing ones more general - leading to ambiguity, bloat and mental load for the existing operations.
Just an aspect.
It’s the kind of reasonable discussion that I crave in this field.
This was just an example of a behaviour that, if taken too far, could eventually lead to those 'hundreds of lines of abstract code'. Of course a single fold in itself is nothing to be concerned about. But an API that accepts a type 'm a' instead of '[Order]' has lost some of its connection with the business domain already.
Again, this might not be bad, it can even be good, but the problem is that many FP enthusiasts overdo it.
> In fact, I’ve seen a lot more unnecessarily abstracted garbage (usually “design pattern” workarounds to limitations in the object model) in just about every oop based web framework I’ve worked with.
Yes, this is the same problem which pops up in the OOP world as well. I guess it is a human thing to want to use your latest powerful language tool wherever you see a possibility for it.
Yeah, I’ve spent way to much time digging into “hundreds of thousands” of lines of abstract OOP code. And it’s sadly, largely ceremonial, mostly due to limitations in the language. The less fanatic about OOP the language is, the more useful it is, is a good rule.
But it’s absolutely not exclusive to either paradigm.
> But an API that accepts a type 'm a' instead of '[Order]' has lost some of its connection with the business domain already.
I completely agree, but I’m also curious how any API interface like that would pass code review. Since it’s so obviously too general.
But I guess the perceived upside is that the equivalent oop-construction would not get caught in code review.
> Yes, this is the same problem which pops up in the OOP world as well.
I know. I’ve spent more time working within oop than without. The type based FP-world just have orders of magnitude better tools.
> I guess it is a human thing to want to use your latest powerful language tool wherever you see a possibility for it.
This is kind of a sad way to dismiss people, as if they’re some kind of ephemeral fireflies...
The reality is that trying to force reality into in an inheritance-based, tree-structured taxonomy, is not only stupid, it also creates endless maintenance headaches.
Some problems fit that model better sure, but most things fit a much simpler ADT-based model since it’s much closer to how we think logically. (In terms of logical primitives and implicit recursion)
This is a great point. I think there are a lot of open questions about basic questions such as how ergonomic effect systems can become and which functional programming abstractions become intuitive through enough practice versus always feeling confusing. I think that stuff will shake out in a decade or two, and we will end up with much better styles to choose from. Who wants to program in 1990s/early 2000s Java style anymore? (Well, okay, a lot of people, but ignore them.) Nobody would judge OOP negatively based on that mess, because we know it can be done a lot better, and languages have evolved to cater to what we've learned. I think functional programming has a lot of evolution to come that will make it more ergonomic and more practical. It will be especially interesting to see what the cohort of programmers introduced to functional programming via modern front-end Javascript will do as some of them transition to the back end and look for statically typed languages that are better than Typescript.
Elm lacks typeclasses, yet I've found this lack is what keeps a lot of libraries at bay that would have otherwise turned out overly abstract (see any mainstream Haskell library, really). It's the same reasoning behind Go: With great power comes great responsibility. At large, programmers shoot their own feet with powerful languages. Therefore, take away their guns, ie make languages less powerful. I still wish there was something like Elm, but tailored for backend/network programming: https://news.ycombinator.com/item?id=21909087
For simple cases, the end result of no typeclasses can be compelling sometimes.
For larger cases, taking away that useful tool to wrangle inherent complexity usually results in more complexity taking the form of gobs more simple code that obsfucates the task at hand.
It is penny-wise in that it saves some code, but pound-foolish in that it makes maintenance harder.
Why? Because when you have an abstraction, your code, possibly in many places, depends on it. Now if you need to make a change, you will need to understand both the abstraction, and your "instance" of it.
Well no rather you need to understand ALL instances of that abstraction, to know that if you change the abstraction you are not breaking ANY of its uses.
Penny-wise and pound-foolish in many cases.
Abstraction is (generally) an investment in the long term health of your system, whether you, the dev, can justify the costs to higher ups or not is usually a pretty big part of the question. But, if there are legitimately abstractable things you can abstract please do abstract them (and add tests over everything).
Don't forget the Opportunity Costs. If you spend much time abstracting, it is less time actually producing something that helps the users of the application.
Now if your business is maintaining an existing application it makes sense you should invest in making its maintenance easier.
But if you are programming a new application you want to get it in production fast, so that you can learn about what needs to be different about it.
I would say that the time to think about maintenance is mostly when you are in the maintenance stage, because of the huge benefit of getting early feedback,
> I want an FP ecosystem that's not rooted in research
Erlang is rooted in practicality. https://www.youtube.com/watch?v=7AJR66p5E4s&t=180s
This feels backwards to me. If I‘m new and join your team I’m much more likely to understand what a monad and a fold are than to understand your domain specific business objects.
So if we are working for an insurance company and see the ‘Insurance’ type is an instance of Monad I now know a great deal about combing different insurance products into new derived ones. By reading a single word of code!
Meanwhile multiple paradigm languages keep getting most of the features that actually matter to Joe/Jane developer.
I don't disagree with this (goodness knows there are plenty of functional purists who come across as religious zealots), but I think it's also worth mentioning that there's an opposing camp of people who are hard-set against ever considering FP for anything at all because they've bought fully into object orientation and stateful computation.
Both solutions are well-suited to certain kinds of problems, and this opposition is well summarized by study of what is called the Expression Problem[0]. A good developer should know both paradigms (in addition to others!) and use each where it is best suited.
[0] There are other issues than those discussed by the EP, but the EP is, I think, the most simple perspective of the distinction between OOP and FP.
Edit: TIL you cannot escape an asterisk on HN. That's a bummer.
In general it feels like the overall feeling to FP is either
"I don't have time to learn a new paradigm when my current one is working good enough to make the company money"
or
"I love using FP ideas in my code when it makes sense"
I really feel like this is the ideal state for Software Development, as either side "winning" will only hurt the robustness of the environment.
ArrayList result = new ArrayList();
for (int i = 0; i < input.size(); ++i) {
etc.
To them that was the "easy" way, and I was a weird kid into weird shit. I didn't make any progress with them at all trying to convince them that "map" and "filter" were simpler than iterating over an index every single time. Now everybody has forgotten that business logic used to be one for-over-index loop after another, often nested, and debugging an error often meant running through a for-loop in a debugger over and over again examining the partially constructed result at the end of each iteration trying to figure out where it went wrong. FP saved us from that, and it gets no credit. People only think of FP as the scary stuff they haven't learned yet.The idea of "referential transparency" is that you should always be able to substitute a reference for its definition, or vice-versa, without changing the behavior of the program. This implies both immutability (so that duplicating a value doesn't change the result) and purity (so producing the value has no side effects and gives the same result no matter how many times you do it). It has nothing to do with knowing what a variable is referencing. If anything, functional programming encourages the use of generic functions which don't know or care what their variables are referencing (parametric polymorphism).
I’ve posted links to this work-in-progress book a few times in the past, but I’m going to do it again because I think it fits the description perfectly.
Grokking Simplicity by Eric Normand [0] is a great book for introducing the premise behind FP in a pragmatic fashion. It doesn’t get bogged down in monads and type theory, just the benefits of pure functions and modeling applications as flows of data.
All the code examples are in JavaScript, so it requires no prior knowledge of functional programming, and it does a great job of building things up from first principals while highlighting the benefits vs a more object oriented approach.
It’s only half complete, with a nebulous completion date, but I’ve already purchased a copy, and it would the first book I’d recommend for any newcomer to FP (it’s already worth it just from the first half as far as I’m concerned).
Like others, I'm eager to hear more of your thoughts.
I also have a theory. A bit of a riff on Rob Pike's observation (IIRC): "Show me your data and I'll understand your code".
Enterprise-y (corp IT) projects are data centric. Functional programming emphasizes flow-of-control over the data flow. Even more so than functional decompensation (imperative programming) and object-oriented.
Donald Norman's notion of "affordances" applies. Any one can certainly "do COBOL" in Scheme, but pushing that rope requires uncommon insight and intention.
A large proportion of this talk is based on the book “Crossing the Chasm". The book was originally written for startups, and this talk adopts the book to an open source audience, especially Haskell developers.
Gabriel Gonzalez – How to market Haskell to a mainstream programmer: https://www.youtube.com/watch?v=fNpsgTIpODA
The showcase example "Circle, Square, Rectangles are kinds of Shapes that have area" represents an abstraction that is present in the conceptual schema (aka Domain Model, aka Universe of Discourse, aka "problem space"), regardless of implementation style.
Likewise, what you call "mathematical abstraction" (and I'd prefer to call a "implementation model") is present both in OOP (eg "visitor pattern, inheritance, facade") and in FP (eg "fold, pattern match, monad"). I don't really think the difficulty in groking "visitor" is much different from groking "fold".
That's related with the problem the parent is talking about: it's very easy to create DSLs in in languages like Common Lisp, so people use them a lot. But for an IDE's parser, or even for a new coder discovering the codebase, it's pretty tricky to figure out what the code does.
If somebody is ultra-familiar with Common Lisp but for some reason never learned how the loop construct worked they might be stumped when they encounter the following code:
(loop for x in '(foo 2)
thereis (numberp x))
What does that do exactly? What will it evaluate? What will it return? Which one of these symbols are special keywords and which one just refer to variables or functions?Meanwhile if you code in, say, C, the macro system is so crappy and clunky that you basically avoid it for anything complicated. Writing a similar LOOP macro in C would be an absolute shitshow (not that it prevented people from trying). As such C code tends to be much more verbose, but also much easier to follow if you know the core rules of the language.
Using Slime, the Common Lisp IDE mode in Emacs, there are at least four ways to answer your loop questions:
1. Ctrl-x, Ctrl-/ to look it up in the Lisp spec
2. Alt-. to jump to the definition
3. Alt-x slime-macro-expand-1 or Alt-x slime-macro-expand-all to see what code the macro generates
4. Execute it in the REPL and see what it does.
Option 1 only works for things defined in the standard, obviously, but options 2-4 work for everything.
``` ourReportAccumulatorFunc :: Monad m => m a -> m a -> m a ourReportAccumulatorFunc = blah
ourFunc :: (Foldable t, Monad m) => t (m a) -> m a ourFunc = foldr ourReportAccumulatorFunc someUnitValue ```
Why do you think a fold would be hundreds of lines outside of the business logic?
Like this.
Not this.Imperative languages have slowly been adopting some of these. 20 years ago you had people talking about how things like map and filter are confusing but nowadays most imperative languages have a version of it so obviously it isn't that confusing.
The killer feature of programming with pure functions is that you can guarantee the same output for the same input. If I were pushing this idea 5+ years ago, I would have said "Unfortunately you have to give up your comfortable for-loops and mutable variables* and learn about these weird maps and folds instead, but it's well worth the trade!"
Fast-forward a few years and now maps and folds are everywhere in the mainstream (it's even recommended to use them over for-loops in many cases.) But we never got the same-input-same-output guarantee which was my reason for switching in the first place!
* Even that's being too pessimistic. You can have your mutable cake and keep your guarantees, too: https://wiki.haskell.org/Monad/ST#A_few_simple_examples
What you describe as a problem sounds like a solution to me :)
I'm curious whether you think there are imperative abstractions that could be used over the commonly used FP abstractions.
That avenue of discussion sounds potentially valuable and fun to me :)
// This is obviously not too right
",".splitBy("1,2,3,4,5")
// This should be right, because it reads out more naturally
"1,2,3,4,5".splitBy(",")
It's funny that in Python split works this way but join doesn't. This is because in the case of split both arguments are strings, but for join one of the arguments is a Sequence, which is a general protocol rather than a class.The proposed solution by Keli is to be inspired by the Smalltalk-style message syntax:
'Hello world' replace: 'Hello' with: 'Bye'
I think, in general, requiring named arguments is a good thing. Swift does it, and in codebases with a decent amount of TLC, it looks great.Function calls are one of the weird places were the syntax of the language rarely helps you figure out what is going on, and for the most part is just a sequence of human-written names one after another, and in languages with custom operators it could be even terser.
In comparison, if-statements, loops, pattern matching, etc. were (hopefully) designed to be expressive. I think by requiring named arguments, function calls will also be much more readable, relying less on an active human effort to do so.
function foo({ bar }: { bar: string }) {
Still a great pattern, but I’d probably use it a lot more if I only had to write the variable names once.Plus they don’t always improve writability outside of IDEs because you then have to memorise the parameter names and in some functions there’s several terms that could equally apply (if you’re using an IDE with hinting then the advantages become moot as the same IDE would hint the order of parameters).
I have had pretty good luck cheating this in a bunch of ways: I use a small font, a big display, and I use a terse programming style.
Once you internalize a bunch of common higher-order functions, you learn how to draw a ton of meaning out of a relatively small number of terms.
I think what we really need is a sort of "lens" system by which we can modify the syntactic appearance of our code without adjusting the semantics, but do these changes on-the-fly. So say you're doing some debugging or whatever and you're gonna be staring at the same 300 lines of code for a few days — so you switch over to "terse" mode and suddenly the named parameters are gone and the variable names are abbreviated (assume a magic system that picks good terse variable names according to your preference). But then when you're done with that section and ready to venture into the remainder of the codebase (or if you're a new developer who's unused to the team's naming conventions or whatever), you can use "verbose" mode that shows the parameter names and whatnot.
I imagine something like this is not obviously straightforward, but it could be worth investigating!
The opposite of named arguments isn’t single character variable names. Any organisation with an enforced coding standard would ensure that variables are descriptive irrespective of whether that language uses named arguments or not.
I didn't mean for my comment to be entirely literal, either. Rather, I just meant to say that terseness can impede readability for those who are not yet familiar with the codebase. (But I have personally been on the receiving end of onboarding into a codebase full of literal single-character names, which I found incredibly frustrating.)
You should really read about APL and other array languages then. (I don't have a good starting point, but they tend to come up on HN periodically such as [0] [1]).
Some companies earn the privilege of a super tenured core team of engineers who work on their product for an extended period of time. They will choose different tradeoffs from a team that needs to adapt to higher turnover.
Thus, you can be writer friendly and reader friendly, considering the reader uses an IDE.
There's also very common sources of bugs when functions take multiple arguments of same (or sadly in some languages, implicitly convertible) types. With named arguments in complex functions, you can sit down, read the code, and spot the bugs. Happens frequently enough in code review that we have a category for it. Without named parameters here, every single function call becomes a game of "mouse over the parameter in the IDE". Moreover, "you can just read the docs inline in the IDE" also causes people to not think much about naming things, which also harms maintainability.
It's a real issue in large, long-lived codebases that may not seem like much in smaller projects.
For example the terse style of Sexpressions or C-style braces are off putting to some but after a short while they become second nature to visually parse (not saying all code should follow Those idioms, just using that as an example of how readability changes with experience). On the other hand I never found it as straightforward visually parsing the “word soup” of named arguments when using languages that favoured it. While those languages were easier to learn the basics, they quickly became tiresome to write larger code bases with.
But I guess this just boils down to personal preference.
<name of function> <value1> <value2> <value3> ...
The absence/presence of parenthesis and commas don't add anything of semantic value here, so that's not really what I'm referring to.
The problem is, is that the values often don't have any meaning attached to them, especially when they are literals. C++ codebases are pretty bad about this with boolean parameters, just having a random `true` at the end of the function call.
Variable names at the call-site are good hints, but we often pass the same variable to different functions, and the name doesn't exactly fit the meaning of each function. This is why I think requiring named parameters, at least by default, is better.
You might have 3 related functions like so:
getStudent(datastore: DS, byId: number)
getStudent(datastore: DS, byAssessmentId: number)
getStudent(datastore: DS, firstName: string, lastName: string, birthdate: date)
Without named parameters, you are stuck with function names like: getStudentById, or relying on convention for naming parameters and often wind up with duplicate names or not knowing if a function is out there because your idea of naming is different from someone else's.Intellisense picks this up too and suggests the 3 different names. Works fantastic.
If you have named parameters, this just works:
`getStudent(classRoom: 15, firstName: "Mary", lastName: "Jane")`
`getStudent(classRoom: 15, firstName: "Mary", lastName: "Jane", middle: "Anne")`
It can call the same function with optional parameters, or 2 different functions and you don't care from the calling side because the syntax is the same and always makes sense.(slight tangent)
Far better with overloading in general is being able to have 2 methods with the same name which return the same thing but have different types of inputs. Like in Elixir, you can have an API callback be `handle(apiResult)`, then have 2 functions, one which handles the error and one which handles the success. Zero logic written to filter out bad-calls, it's just the same method with different types (the Error result type and the Success result type). Vastly simplifies and cleans up that kind of code.
Students |> Students.Repo.get_by(first_name: "Ryan")
from s in Students where firstName = "X" select s
Students.AsEnumerable().Select(s => s).Where(s => s.Name == "X")
etc.I really loved using Elixir, very frustrating going back to TypeScript after that.
Of course I understand a name could become rather verbose in your last case, but I'm not satisfied that it would be much of an issue in practice.
Like what, precisely, does the autocomplete show in your editor when you type `getStudent` vs `getStudentBy` that makes the former so much better? In either case you are left to disambiguate either the correct overload or the correct method unless... well... you already know there is an overload that accepts the data you happen to be working with. Presumably you would also then know the correct method name.
I suppose it's possible to start typing your named parameters directly and the editor "fills in" name you are going for? But that poses nearly the same problem again: "Wait... is it 'nationality' or 'country'? Did we decide on 'lastname' or 'surname'?"
I agree the `getStudentByCountryAndName` isn't a great signature, and that wrapping all of the args into a single DTO like `StudentInfo` is not so great either. But I can honestly say, I have never once run into a problem of finding a method in the wild! Even if we had numerous `getStudentBy`-type functions all with similar signatures/names it probably wouldn't take more than a few seconds to narrow down to the one of interest.
I also find, from a consumer perspective, named-parameters to be less ergonomic. I like that when I type `getStudentById(` my editor boldens the argument I am meant to pass next. I don't have to decide which parameter I am going to pass next... my editor just walks me through the function (maybe I'm just lazy!)
To be clear, I am not arguing against named-parameters. Though I dislike that they broaden the public surface of functions, I suppose I would rather at least have the option to use them than not at all. I just don't really see, in a practical sense, how they would improve a code-base beyond some fairly niche scenarios that are probably a code smell anyway.
[0] It seems to me your argument is more aimed towards overloading than named-parameters in particular, but I will accept that there is significant overlap in this space.
Why would you want a function that gets students to be called anything other than `getStudent`? Fundamentally, the simpler, easier to parse your function name is, the more readable and editable your code is.
I definitely think overloading functions is more interesting/ important than named parameters. But named parameters are useful both for overloaded functions and for optional parameters where you might not know what the 3rd/ 4th parameter might be.
One pattern I see frequently in languages which lack named parameters is instead of using optional parameters, people pass objects with optional properties. This is used a ton in Javascript/ Typescript. If the language supported proper named & optional parameters, it wouldn't be an issue.
I'm thinking of how you could write in point-free style or even have a `(.)` function to begin with if you had the Smalltalk "message passing" style.
Sure the Haskell syntax is a bit much when you're not familiar with it but, like almost any language, it's often the least-interesting part of the language and the most talked about.
A lot of functional languages -- Haskell as I understand it is the biggest perpetrator -- optimize for code length and composability, trying to make things as expressive as possible, and decrying those who can't read something that has meaning densely packed into every single character as those who are "simply not familiar" with the language.
But that's what OP is saying: OO languages go for readability and not expressiveness, and wouldn't it be cool if there was an FP language that did the same thing?
I'm curious if Keli plans to maintain function composition in the face of named arguments. It would be quite nice to have both.
Moreover, I believe that in general, shorter code is more readable code. People just like to make excuses in order to avoid learning anything new.
let q = (f .y g)(z=a) then q(y=b) = f(x=g(z=a), y=b)
let q = (f .x g)(z=a) then q(z=a) = f(y=g(z=a), x=b)
Aesthetically it would be kinda like the notation for fiber product.
Named arguments improve readability of a code if you are not used to a codebase. By improving readability, you also improve maintanability: you need less experience in a codebase to make a change.
You focus on writability where it's not the biggest issue of the field, it's software debt.
I get each developer is different but personally I remember numbers and orders easier than names.
And frankly, if you have a developer who can’t learn the basics of the language they’re employed to work on yet also refuses to use any developer-friendly tools to assist him, then that’s a failing of the the developer and not the language.
JavaScript's more recent structuring/desctructuring arguments has made this pretty seamless as long as you name your variables well.
For the most part switching from ordered to named is just func(arg1, arg2, arg3) is just foo({arg1, arg2, arg3})
You may need to rename or pass in the params on the call but it's generally pretty nice. As an above comment says about Clojure - as soon as I need a third argument in a function I switch from ordered to named and its pretty painless.
It is a fair complaint, but don't you think this is offset almost entirely by a good IDE?
Second, if a language is heavily reliant on features from an IDE, yeah, I'll echo that's a badly designed language.
Back in my days™ that's what they used to call an "IDE". But today that's just "glorified Notepad++", I guess? What features does it miss that stop it being a full-blown IDE?
Maybe I'm just a spoiled brat, but I came to Java from C, PHP, Visual Studio (the old one) and learned how nice programming could be before I left for .Net Core and frontend (with much TypeScript), mostly in VS Code but sometimes in Rider and Webstorm.
To me, to be a real IDE these days you need to have proper refactoring support.
And to a spoiled brat like me that excludes almost everything except Rider, Visual Studio with Resharper, NetBeans, Eclipse and IntelliJ :-)
But yes, I too am old enough to remember back when the C/Assembler program we used to program microcontrollers were considered IDEs.
And technically you are of course correct :-)
I guess a better classification would be: IDE level 1,2,...n where what I call IDEs today are really level 3 IDEs.
splitBy "," : String -> List String
so it seems "clear" that the separator should be the first argument. But maybe I've just programmed functionally for too long?As an alternative, perhaps there's space to introduce types to make this something like
split : Pattern -> String -> List StringThe 'splittee' should be the last argument. The last argument is usually what you want to a) elide as part of function composition, or b) loop over (the tightest).
E.g. Splitting a file into lines:
lines content = splitBy "\n" content
can be: lines = splitBy "\n"
If splitBy used the other order, it would be: lines = (\c -> splitBy c "\n")
E.g. Splitting a file by lines and tabs: entries content = concatMap splitBy "\t" (splitBy "\n" content)
can be: entries = concatMap splitBy "\t" . splitBy "\n"
If splitBy used the other order, it would be: entries content = concatMap splitBy (splitBy content "\n") "\t" lines content = content .splitBy "\n"
lines = (.splitBy "\n")
dec x = x - 1
dec = (- 1)
It seems like some infix-like things ought to be equivalent to (oper right left) rather than (oper left right). Not sure how that works out in practice, though.But that doesn't actually solve the problem.
"Hello world".replace("Hello","Bye") // works fine
but
somesequence join: "," // still doesn't work
// unless every $Sequence implements join explictly -- Is this correct?
splitBy "," "1,2,3,4,5"
-- or this?
splitBy "1,2,3,4,5" ","
there really is no question that the first version is correct. Even oversimplifying as the article does and treating "basic English" as the only applicable domain knowledge, I'm not likely to read "split by ',' …" as an instruction to split the string "," by some other separator. To understand how the second string fits in you need more than basic English, because English doesn't have that sort of syntax. However, once you learn that it parses as `(splitBy ",") "1,2,3,4,5"` the result is pretty obvious.In the OOP language example the second version with the string knowing how to split itself, as it were, does make more sense than the first version. The target of the method is always the subject, and the method name is usually read as a verb, with parameters as direct or indirect objects. (Read as: String "1,2,3,4,5" (S), split yourself (V) by the string "," (I.O.).) However, in functional programming the bias is exactly the opposite, because functional programming is about programming with first-class functions on data, not performing actions. You could easily arrange to write:
"1,2,3,4,5" `splitBy` ","
in Haskell using infix notation, and it even reads fairly well reinterpreted as English prose. However, defining `splitBy` this way would imply the curried prefix version: splitBy ","
would not be a function that splits its input by ",", as one would expect, but rather one that splits "," by its input. Which just goes to show that not every function is well suited for both infix and prefix notation. Whichever version you choose needs to be used consistently. In general the convention has been to define multi-parameter functions such that they can easily be used as combinators, via currying, which seems fitting to me given the nature of functional programming. Functions which are designed to read well as infix operators usually require operator sections or `flip` to adapt them for use as combinators.> I think, in general, requiring named arguments is a good thing.
I have no objection to optional named parameters, but making them required would necessarily eliminate currying and simple function composition, and that I have a problem with. Record parameters with named fields are generally sufficient for the situations where named parameters are useful, especially since they're ordinary data which you can manipulate at will and not some special syntax baked into the language just for function calls.
It occurs to me that this could have been avoided at the language level by defining infix notation to implicitly flip the arguments. For example,
x / 3 == (/ 3) x == (/) 3 x
x ++ "suffix" == (++ "suffix") x == (++) "suffix" x
x `splitBy` "," == (`splitBy` ",") x == splitBy "," x
(The left and center forms are valid and equivalent Haskell code today; the right form has the arguments flipped.) I think on the whole this would have made it easier to transition between the curried forms and the infix forms, since it's more common to want to bind the RHS of an infix operator than the LHS. I would also say that e.g. `(/) 3` reads more naturally as "divided by three" than its actual definition as "three divided by …". No doubt we could find a few counterexamples where it reads better to have the LHS first, though I can't think of any at the moment.Of course, it's much too late to be changing something so fundamental at this point. Even as an extension it would cause far too much confusion.
It makes me really sad that Elm turned to this niche language/framework that it is today. I had hoped for it to grow someday to have a mobile target in addition to web, to be able to use it on the server, hell even to be able to use it to build systems. So much wasted potential.
The whole reason Elm has been stuck in a niche when it had _huge_ hype around 2015 and everyone was sure it would be the "next big thing" on the front-end is that the developers have tried to keep full control of the language and keep shooting down proposals by users. It's either their way or the highway.
I went through and collected up the reasons to do it, distilled it down and gave examples as requested by the core team and got lots of positive feedback from the community, and just got no response from the core devs and nothing happened.
I agree not all changes people propose are good, I like the fact the language is opinionated and doesn't include half-baked ideas, but there is literally no way to get anything through into Elm core, any attempt to do the legwork is just ignored and thrown away. It is a one-man project and that's it, but they pretend that isn't the case.
Honestly, the answer is probably an unstable fork of the language for more experimentation and development, which to be fair, anyone could do¸ but obviously maintaining that would be a lot of work (I don't want to imply that the core Elm team have some responsibility to do it).
I even got a proof of concept working and showed it off in the Elm Slack.
And then I got told my library would not get accepted on the official package repository because the core team was developing their own "correct" abstractions for the main Web APIs and they did not want third-party libraries to compete with the core team's libraries, as regular web developers don't know what they're doing and are incapable of choosing the "right" abstractions, which apparently only the Elm core team is capable of doing.
That experience turned me off Elm for good.
Flash forward to 2020: There is still no "official" Web Audio library. (There are a few non-official ones on Elm Packages)
It's your right to side with the core team, but they're the ones pretty much killing the language's chance to see any real usage.
F# works great with Visual Studio, and especially well with intellisense.
F# was designed as industrial language from the beginning, and high quality tooling is a large part of that.
It might just be me, though, maybe other people use the language in ways that don't work that well there.
> Keli is created to overcome a problem that is almost undeniable in every functional programming languages: the lack of proper IDE incorporation (perhaps except F#). In short, Keli can be viewed as an attempt to assimilate the Smalltalk's syntactic model into a functional programming environment.
No .NET Native, GUI designers, WCF/gRPC, EF designers, Blend, WinUI tooling, ASP.NET templates,... hinders adoption at corporate level.
Ironically, despite .NET core being an excellent runtime, it suffers stigma from being assumed to be too tied to Microsoft platforms. This means, depending on your angle, F# lacking specialized ties to the Windows specific tooling you mention need not be considered a disadvantage.
They cite positional parameters (that they call "prefix notation"… i wonder why ?) but ocaml already has support for named parameters (you can write things like split ~on:"," "a, b, c" for example) Moreover, guessing the order of parameters is generally not that hard :
— If you have good intellisense your IDE will tell you which parameter is which (how they are called in doc)
— If you have good intellisense, your ide will tell you the type of function, which, with strict typing is sufficient in a lot of cases (think "send : Unix.socket -> string -> unit", the type tells you the order of arguments)
— with named parameters, you don’t have to look up the order of parameters… but you have to look up their names, same problem
— This last one is pretty subjective, but in a lot of cases you can guess the order of parameters by thinking "on which argument does it make sense to do partial application ?" `splitBy ","` makes sense, `splitBy "a, b, c"` doesn’t really… so "," is the first argument (but that is subjective)
For IDE support, with the right tools, in my experience it is pretty good (ocaml-lsp or merlin do the job pretty well, and strict typing often allow for better intellisense than on non-functional languages)
Though there we arrive at the real problem I’ve experienced : the tooling. It’s not actually that bad. more like it’s badly explained. how do you know that you are supposed to install
— opam for package management (and reinstall ocaml via opam)
— dune for building
— utop as a REPL, because the basic REPL sucks
— batteries or janestreet-core for "standard library" because the standard library sucks (and which one anyway ?)
Plus these tools are hard to use at beginning / have a learning curve. (why can’t creating a project be as simple as - type "cargo new project" -> now you automatically have a project dir with a local opam switch, an opam file, a dune file, an example program, and basic utilities installed in that switch ? )
Personally, I think that’s what fp lacks the most right now (though I’m probably influenced a lot by ocaml) : easy to use and flat-learning curve tooling
On the topic of and janestreet-core/base and named parameters, I don't like how it basically forces you to name your arguments whenever you need to compose functions. I much prefer Containers library which is lighter and stdlib friendly.
* Base/Core: List.range 0 20 |> List.map ~f:(fun x -> x * 2) |> (fun l -> List.take l 5)
* Containers: List.range 0 20 |> List.map (fun x -> x * 2) |> List.take 5
For something Keli has that Ocaml doesn't: it is interesting to see a statically typed FP with multiple dispatch built in from the beginning.
My observation was that to feel comfortable in Java I tended to require a very powerful IDE (Intellej) to deal with refactoring and appeasing the type system.
When I write Elixir, I feel comfortable using a much less powerful (from a language integration standpoint) Vim, mostly due to the constraints Elixir has in the language. In Elixir, there is no mutable state, I can feel confident that the only things affecting a function are the things in front of my face when reading it. Elixir's alias/import syntax make it pretty easy to jump to the file that has a function definition in.
I think FP languages tend to have an edge in how much power an IDE is required to have, because those languages tend to have features like immutability and composable higher order functions)
My flow now is to search for a bunch of instances using the fzf plugin (https://github.com/junegunn/fzf.vim) and open them into the quickfix list, then do something like
:%s/old/new/gce | :w | :bnextThe two features I really like about fzf are
1.) the preview window (visible here https://github.com/junegunn/fzf)
2.) Fuzzy finding/multi-selection/gradual refinement of search terms
Plus, named and named-optional parameters are supported in the language, and can be used to good effect for disambiguating function parameters of similar type (the split, join problem).
I love OCaml as a language but we have to admit that the developer experience just sucks.
For something to be simple, a 10 year old kid needs to be able to figure it out. That’s how languages get adopted.
(I feel obliged to give an example of what I think is a bad developer experience, and I would point at Haskell. There are far too many toolchain variations to choose from, all of which seem to be in active use -- cabal, cabal-v2, stack, cabal-v2+nix, stack+nix; ghcid, ghcide, etc. etc. Once your choices are made and your stack is configured, you'll be okay, except for the inordinately long build times. But in my experience, it can be a real nuisance to come back to a half-finished Haskell project -- say, on a new machine -- unless you've made a singular, personal commitment to a specific technology stack. Leave yourself copious notes, Makefiles, and shell.nixes to remember how to get the thing running! But this is a personal sob-story and a digression.)
My main argument -- which I think is being lost here -- is that Ocaml isn't IDE unfriendly, and especially not so unfriendly that an entirely new language is needed just so that FP and IDEs can happily coexist.
(Well, except for Windows. Ocaml + Windows is more unpleasant than it needs to be.)
Right, and I guess my counterargument would be that, yes it is, and you don't need to resort to logic to see it - just do a little googling about its poor adoption.
>Compare that to JavaScript where you can literally just install vscode and node and you’re up and running
As opposed to OCaml where you... install opam and merlin and you're up and running?
>Not to mention that you could pop open a console in any browser and execute code.
As opposed to OCaml where you can pop open utop and execute code.
>For something to be simple, a 10 year old kid needs to be able to figure it out.
How many 10 year old kids are there working as professional programmers? Come on...
Many of them in 10 years time, and they will be using concepts they learned when they were 10. And FP enthusiasts would complain why no one uses their favourite language.
My point is, if we want to get more people to use FP we have to make it more accessible. Everything from building better tooling, tutorials, environments. Good languages alone don’t matter.
You can’t tell me that it’s just as easy to get started with JavaScript or Python today as it’s to start with OCaml. That’s just delusional.
If you want to expose 10-year olds to functional programming, Logo [1] is a great place to start, and has been for decades. That's where I picked it up (I didn't know it at the time, but looking backwards it's now clear).
[1] https://en.wikipedia.org/wiki/Logo_%28programming_language%2...
Have you used OCaml? Because you can set up a complete environment with IDE support in like 5 minutes flat. It's actually one of the easiest environments to set up of any language. dune, merlin, opam, and utop provide everything you would possibly need.
That's only because VSCode was developed in JavaScript, and JavaScript is used in Browsers anyway. This has nothing to do with Intellisense friendliness.
In that case there are many functional languages with good IDE support. E.g. F# through Ionide.
Most languages at most have autocomplete and definitions look up (and even those are often very brittle). That is a far cry from what an IDE should be able to provide.
Granted, recently I only tried OCaml via Reason, but I remember that some of the errors the compiler produces are not IDE-friendly either (something like "Type A provided but elsewhere wanted B" with no indication where :) ). But that's mostly due to the age of the language (most "old" languages are very user-hostile and have very terse/cryptic error messages).
I can't speak to Ocaml in IDEs other than Emacs, but in my experience, error messages are quite location-precise, down to the specific word(s) on the line causing the error; and the messages are readable with a little practice. They are not as good as, say, Rust's error messages, but you get used to them fairly quickly! But that's more a condemnation of the compiler, not of the IDE.
There are a few esoteric error types in Ocaml ("Expected a value of type 'a, but you gave a value of type 'a" -- OK, that's a head-scratcher) but those are very infrequent in typical code.
I have hoped to have eliminated all of them in 4.08.
It seems to me there's a lot of focus on syntax, and I don't think they've gotten to a great place. For example:
(this Int).square | Int = this.*(this)
In Rust, they do impl Int {
fn square (self) -> Int {
self * self
}
}
I can read the Keli version, but I'm note sure that `(this Int)`, `|`, and `.*` are great inventions. You need a good reason to deviate from commonly recognized syntax, and Keli doesn't feel like it's made things simpler with this.In Dark, we use piping heavily to get the benefits of the dot-syntax. In particular, it can be type directed:
8 |> Int::square |> Int::log 2
The autocomplete will only show you integer functions in this case.For the problem of "which parameter" - our IDE shows parameter names when your cursor is in a position to type the next parameter, so you know what you're adding it is. We also show the parameter names when your cursor is in the completed argument so you know what parameter it is. (I couldn't see if Keli did this, it might not be possible in modern editors).
myList
.append(5)
.reverse
.put(5) atIndex(0)
.++(anotherList)
.select(x | x.isEven)
Edit: The same feature makes it look a little weird when put on a single line, because we mentally parse the whitespace as separator but in this case it is not: $.name("Keli") age(50).isOld
It looks like a refreshing addition to function programming languages and I'd like to see where it goes from here, in terms of adoption and features.wold've been nicer to me but ah well.
The two downsides mentioned regarding FP are not an issue if you are using a REPL and a Lisp:
- excellent IDE support, since you are working inside your program (which can be inspected)
- paradigm a la carte: you want named arguments? Go for it (split :string "foo,bar" :by ",")
you want different invocation syntax? (3 + 4) or (3 4 +) is just a macro away
and a bit easier to read than: (this Int).square | Int = this.*(this) YMMV you want to define your data shape? use your favorite schema language
you want monads? just use a library
you want go-routines? just use a library
you want compile time types? just use a library // This is obviously not too right
",".splitBy("1,2,3,4,5")
// This should be right, because it reads out more naturally
"1,2,3,4,5".splitBy(",")
Seeing as Python uses the first version for .join() Python 3.8.2 (default, Jul 16 2020, 14:00:26)
>>> " ".join(["a", "b"])
'a b'
vs Ruby 2.6.5 :001 > ["a", "b"].join(" ")
=> "a b"
I don't know which one is more natural but I prefer the Ruby version because it's consistent with "a b".split(" ")
which works in both languages. One less think to remember.["a", "b"].join(" ")
This would mean the type list has a method join, how would it work with the following ?
[1.5, "hello", None].join(" ")
>>> f"{1.5} {'hello'} {None}"
'1.5 hello None'
Ruby does that. From [1] "[Array#join] Returns a string created by converting each element of the array to a string, separated by the given separator."The difference is that nil (Ruby's None) disappears
2.6.5 :001 > [1.5, "hello", nil].join(" ")
=> "1.5 hello "
That's totally expected because in Ruby the conversion of nil into a string is the empty string. Python converts it into the string "None".[1] https://ruby-doc.org/core-2.6.5/Array.html#method-i-join
Edit: an example with a type that normally wouldn't meaningfully cast to string
2.6.5 :001 > class Example
2.6.5 :002?> attr_accessor :name
2.6.5 :003?> end
=> nil
2.6.5 :004 > e = Example.new
=> #<Example:0x000055a321fa2bb0>
2.6.5 :005 > e.name = "example"
=> "example"
2.6.5 :006 > e.to_s
=> "#<Example:0x000055a321fa2bb0>"
2.6.5 :007 > [1, e].join(" ")
=> "1 #<Example:0x000055a321fa2bb0>"
2.6.5 :008 > class Example
2.6.5 :009?> attr_accessor :name
2.6.5 :010?> def to_s
2.6.5 :011?> "#{name}"
2.6.5 :012?> end
2.6.5 :013?> end
=> :to_s
2.6.5 :014 > e = Example.new
=> #<Example:0x000055a321f8a4c0>
2.6.5 :015 > e.name = "example"
=> "example"
2.6.5 :016 > e.to_s
=> "example"
2.6.5 :017 > [1, e].join(" ")
=> "1 example" >>> " ".join([1.5, "hello", None])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected string, float found
Why would `[1.5, "hello", None].join(" ")` be any different?Why would you put a method on a class if only a small subset of it can use it?
in python, `join` is implemented in the `string` class, and the argument is an iterable. therefore, if your class wants to support `join`, it needs to implement the `__iter__` method (python's equivalent of ruby's `each`), but it does not need to also mix in an implementation of `join`.
it's mostly a cultural difference between ruby and python - the ruby ecosystem leans more heavily towards mixins and implementing generic functionality by attaching methods to objects, whereas the python ecosystem leans more heavily towards implementing generic functionality by providing functions (or methods on an external class) that accept generic objects.
A class that wants to support join has to implement to_s. The definition of join is
> join(separator=$,) → str
> Returns a string created by converting each element of the array to a string, separated by the given separator. If the separator is nil, it uses current $,. If both the separator and $, are nil, it uses an empty string.
If the class doesn't implement to_s Object.to_s kicks in and displays something like "#<SomeClass:0x000055a321fa2bb0>"
Enumerable is really a mixin https://ruby-doc.org/core-2.6.5/Enumerable.html but join is not listed in its methods.
There is another reply of mine with an example of a class can be used in join and doesn't mix in Enumerable.
Consider this crude csv parser:
using splitBy:
csv.splitBy("\n").map(csv_row=>csv_row.splitBy(","))
using split: "\n".split(csv).map(",".split)
split was the clear winner here. Given splitBy, I basically recreated split as a lambda.I tried to create an analog to that where splitBy would come out looking better. I figured that if we didn't know the dimensionality of our data, then delimiters becomes an array of arbitrary length and we could pass data.!splitBy into something like delimiters.reduce. When actually writing that, however, I wound up recreating split again:
using splitBy:
delimiters.reduce((accum,delim)=>accum.deepMap(data=>data.splitBy(delim)),[data])
using split: delimiters.reduce((accum,delim)=>accum.deepMap(delim.split),[data])1 + "hello" == "1hello"
Therefore, the type Array can have a join method that have predictable results (unlike Python).
As mentioned above, it's because Python supports `join` on any sequence, not just the array class. Personally, I find JavaScript's solution neater:
[..."hello"].join(" ") === "h e l l o"Javascript always falls back to string, that is also why we have === and == operators.
Python's type system is a bit more strict because it will not attempt to serialize your objet if you haven't explicitly done it yourself.
My point is: Python doesn't use this notation because it would not make sense in Python.
>>> [1,2,10].sort()
Array(3) [ 1, 10, 2 ]'Hello world' replaceFromIndex: 0 toIndex: 4 with: 'Bye'
At first sight you might think, oh great don't even need to read the documentation (which is the author argument in favour of this). But, is "toIndex" inclusive or exclusive? Don't know, so now I need to read the documentation anyways and just learn the behavior of the function. And once I did, all those names just become visual noise that my eyes need to skip over when reading and verbosity when writing.
Also, if you're going to go with this style, I'd much rather have function first (as FP should):
replace in: 'Hello world' fromIndex: 0 toIndex: 4 with: 'Bye'
Which brings me to my second point, I prefer languages that just allow both. Some functions are better positional and other named and some a mix of both. Let each function choose the most appropriate one. For example, are we really going to do this?
100 divide with: 5
or
divide theNumber: 100 with: 5
Ya, it reads like an explanation for a 5 year old, but as a professional programmer I'd much rather:
divide 100 5
My point being, certain functions are intuitive even with positional args and others arn't, only the latter should have named args.
Now for the intellisense section I agree, but I think you can solve it with IDE UX. Like just provide a command that lists all locals for you to pick and then lists all functions whose first argument is of that type. Or heck, have it that after you type a var name, it lists functions over it but the list shows to the left and when you pick it autocompletes the function text to the left of it.
The only unconventional thing is that the function might be put in-front of the value and some parentheses need to be added, instead of just putting it after the cursor like with method syntax. I assume you get used to that rather quickly.
On the contrary, this should give Haskell an advantage because all the types are known and the IDE should be able to autocomplete only the correct parameters. But that requires compiler-as-a-service, and GHC is not. And writing your own analyser for a language is quite a task.
Yep. Haskell-language-server is very nice, though there are some performance issues I've experienced with a template Haskell heavy large codebase.
For many cases though, the experiences is very smooth after install.
Install issues are close to being sorted out with static binaries being downloaded in the vscode extension for instance.
(the following is just what I think, I have no hard knowledge on the matter)
Language server still limits what an IDE can do. E.g., IDEA can do complex large scale refactoring and analysis over a codebase. If you're brave enough, you can write your own language analyzer that converts your language into structures IDEA understands, and you can get (some) of the same benefits. But it's a lot of work.
IIRC, language server can only provide rather superficial type information and autocompletion (which is still way more than many languages had before).
That also makes the language not about a "good user experience" as given in the docs, but "being compatible with modern IDEs". No thanks, I would rather improve tooling for an existing language that already has a community and ecosystem.
But yes, I agree since the author specifically talks about the way OO languages provide a more easily autocompleted syntax and parameter help doesn't really differ, perhaps the better term to use here would be autocomplete or code completion.
Maybe it's trying to refer to stuffs such as existing libraries and package management etc. when you want to quickly boot up a real-world project. I once tried to write some web app in Haskell and that experience definitely was anything but smooth. Fortunately Elixir has a really vibrant community and libraries for the majority of common tasks (plus 20+ years of Erlang ecosystem). It's also really easy to roll your own solutions.
I don't think the issues of IDE support and parameter ordering mentioned in this article are problems to FP programmers at all. Powerful language servers exist nowadays for all major functional languages and they're not that different from language servers for OO languages. So yeah I tend to agree with a lot of commentators that this article doesn't seem to make much sense unfortunately.
It also mitigates the parameter naming problem. With a type hole, the language server will infer the type. But in the case of two parameters having the same type, you have to consult the definition or doc.
Syntactic window dressing isn’t what makes or breaks a language.
> Ambiguous functions argument positions
It just seems to be advocating for named arguments, something which the majority of languages (OO or FP) support these days. You could say that requiring named parameters is a feature but it's hard to get excited about.
> Not Intellisense friendly
I use Elm and Elixir, both of which have decent autocomplete in vscode (and I believe many other editors).
Critique aside I definitely applaud more towards increasing FP adoption. I think Elm has brought many good ideas to the table already (though admittedly it's bias is more towards ideal than practical). On the flip side Elixir is very pragmatic and easy to get going with for developers coming from OO languages.
EDIT: Downvotes already? Look, Scala is NOT noob-friendly, and it's a rational argument, not a preference. Here's an example: There are at least 10 (TEN) different uses of the underscore character (_) in Scala. JFC. I can go on, such as the proliferation of bizarre operators everywhere that are impossible to Google (again, not noob-friendly), the reliance on JVM (ewww... so you end up having to be a Scala expert AND a Java expert... plus deal with JVM windup time and JVM stacktraces... Not noob-friendly), the consistent violation of POLS, etc. etc.
> the reliance on JVM (ewww... so you end up having to be a Scala expert AND a Java expert... plus deal with JVM windup time and JVM stacktraces... Not noob-friendly)
This is just plain weird. This makes me feel like you spent zero time actually working with the JVM and it's incredible ecosystem. Being fully inter-operable with Java is a huge win for both adoption and usability. Not only that but the JVM is one of the most mature runtimes in existence with massive amounts of documentation and help available.
It might have some justification, but it's just your preference. Scala gained a reputation of being a "difficult" language early on, I suppose mostly by people coming from Java, and it has unfortunately stuck.
Singling out underscores in Scala as "difficult" is bizarre. There might be many, but in practice it's very easy to understand what you want, and I've never seen anyone seriously confused by them. What do you mean, "many bizarre operators"? Are you using ScalaZ maybe?
What do you mean, JVM stacktraces are not newbie-friendly? They are a useful tool to troubleshoot many problems. What problems do they have in your opinion?
What's wrong with relying on the JVM?
Scala type signatures are sometimes hard to understand in library code. You are not expected to write code like that unless you are writing general purpose libraries, which you are likely not doing.
It's that the user needs to learn not just Scala but also Java whenever they want to do anything "real". You might say this is a minor hangup, but for learners having to context switch back and forth between Java and Scala and dealing with interop issues can make for much more cognitive load than using a non-hosted language like Python or Go.
Scala is eminently beginner friendly when introduced to developers familiar with OOP. This approach was how I first experienced Scala and it ended up becoming a gateway drug into FP proper.
Regarding underscores in Scala, yes there are many technical terms for each thing it has the capability of representing... but in reality, you just use it wherever you want to ignore a value with a placeholder or pass a provided value through.
Scala newbies find it hard to even perform basic Array operations. You need to dig into Array, Seq, and List, figure out the differences, try to see what's accepted by the community, etc.
Scala is probably the most flexible and powerful statically typed language currently in widespread use and I would probably choose the language for most non-scientific computing greenfield projects.
In my experience of on boarding developers who didn't know Scala or Java into a large Scala project, it's really not that hard to learn. Developers were writing okay code in the first week and completely idiomatic code after the first couple weeks.
The JVM does suffer from non-transparent performance characteristics, but the highly optimized and tested platform makes up for it.
Scala the language is getting better and better and Scala 3 will really be something special, I think.
The worst part of the JVM is dependency management. Upgrading the version of Scala you're using is unbelievably painful and in a lot of cases impossible.
My old Scala shop is still running on 2.10 because no ones bothered to put in the weeks of work to upgrade.
splitStringBy "1,2,3,4,5" ","
^ action
^ Arg1
^ Arg21) Function argument order is not an issue in practice. Especially Elixir or OCaml providing keyword arguments. Also, there are mostly conventions eg search term comes first in replace function.
Elixir, F# all provide Pipes, while little more verbose than dot notation, more universal than methods on objects.
2) IDE support: above points apply, although pervasive type inference may make few things hard. It is best practice to annotate function signatures.
The real reasons why FP languages are unpopular are:
1. immutability is often a bad abstraction and some FP languages often make performance/memory reasoning harder[0].
2. Your 9-5 engineer doesn't want to learn anything more than what they learned in school or bootcamp.
3. The impression created by some FP language communities. There are some people always talking some complicated category theory stuff that's rarely useful for programming, or propagating zero information statement like "If it compiles it works". [1]
[0] "bUt ItS lIKe gRaDuAtE aBsTrAcT mAThEmAtICs it must be good and elegant even if the machine doesn't work like this". Go and learn some real computer science, algorithms, data structures, number theory, probability, instead of touting your definitions of undergraduate logic and set theory.
[1] There is more chance it works, sure. But "If it compiles, it works" is dishonest statements.
Yeah, unfortunately, the cheap webshits use reverse loops because they read in some medium article that comparison to zero is shorted, while repeatedly concatenating strings in an inner loop.
Doesn't help with things like - indexing on utf-8 strings is O(n) and often the compiler/JIT is not able to figure out all necessary loop fusions for you.
Basic algorithms, data structure knowledge is a must for every programmer. I don't say they should know how to prove a treap is 0.yz times more efficient than a skiplist. Now the next part.
> while the elegance of functional/mathematical reasoning ...
There is natural reasoning and there is category theory stuff that only serves to signal the dude has studied some aBsTrAcT mAtHeMaTiCs in programming communities.
A best example of this is Haskell. They bastatdized quicksort to show an "elegant example" on their website.
Another example is react. "React is fast omg virtual DOM". Not everyone will have latest macbook pro like hipster webshits.
I know, and I acknowledge the improvements in craft of ~~CRUD WEBSHIT~~ programming that came from FP camps. But there is also lot of bullshit there in FP camps.
- Argument positions are named, help text, validity checking, special values, etc are defined for each argument.
- Command arguments can be prompted within the editor (intellisense).
As400 commands can also be prompted and executed by a user, thus providing a common user interface for running jobs as well.
I've seen nothing like this in the PC world. The two issues the Keli language is trying to solve were addressed 30 years ago on the as400.
The argument that you need method-style syntax to have good IDE support is short-sighted. Haskell has something more powerful (typed holes), so a common thought is Haskell needs a different sort of IDE support and not expect it to feel the same as Java.
Well yes except Smalltalk, the original OOP-language. Smalltalk requires you to use KEYWORD METHOD-NAMES whenever you have > 2 arguments. That makes Smalltalk code highly readable.
(I told you so 20 years ago today) :-)
Rust has amazing sites like arewegameyet or arewewebyet.
A single paragraph around what users can realistically do with this language and what are the most used packages in the ecosystem will be a great help to anyone curious about it.
Haskell code can be optimised to run very fast indeed, but resulting optimised code does not look much like idiomatic Haskell at all.
It's still Haskell though, which means it can compose nicely with all that idiomatic Haskell!
Further I'd argue that you typically don't have to go to that level of optimisation.
I'd be very confident in my above statement replacing node or Python REST apis at least, but can imagine that not holding true in other domains.
In that case though, my first point still holds and I find that advantage very powerful.
Sibling comments aside (without comment on my part), it would be hard to have 6 years of production use of a language that has its first git commit 2 years ago.
"ACTUALLY! ..."
That'd actually be pretty helpful for initial prototyping, where you want to have state shared across devices but don't want to bother with hosting a backend at first, and localhost wont work because the client wants to play with it too.
set the delimiter to comma
split 1,2,3,4,5
or split 1,2,3,4,5 by commaAfter the breakup, fork, rename to the new girlfriend name, and put a notice on README.md:
"I broke up with Keli but don't worry, I am with Sandra now. Please follow the new language <a href="https://sandralang.org">here</a>"
Five years later...
Sick of all the hassle you have to deal with regarding <formerly new technology>...?
The lesson here is ultimately all technologies suck, but you have to find the variety of suck you're willing to live with.
Alternative view is that they all have their uses and if you choose the right one for the use case, it makes your life easier, not harder, especially once you get to know it well.
It helps if you know what do you want. Not a single one is good at everything.
"Did something happen?"
"Sorry. I just had the wrong idea about you, Kaley."
Her: "Why did you change the name?"
Him: "Because I'm hedging my bets in case our relationship does not outlast my pet programming project".
Her: "..."
[0] https://biblioweb.sindominio.net/telematica/open-sources-htm...
If I want to do imperative programming, I can do that just fine in most languages, except for functional programming languages.