lainchan / λ

Favorite feature

Joey · 2022-09-14 11:53
What is your favorite feature in a programming language?
chadols · 2022-09-14 12:52
Compile-time evaluation and metaprogramming.
bit · 2022-09-14 14:42
CL's synergy between powerful homoiconicity, unhygienic macros and gensym. I'm also the anon constantly banging on about how optional named parameters and cruelly underrated.
Batou · 2022-09-14 18:28
Namespaces are a big one. I didn't appreciate just how valuable they are until I'd worked with C, which doesn't have them.
Jens Hansen · 2022-09-15 00:42
>>32751 Took the words right out of my mouth
Roger Pollack · 2022-09-15 10:47
Static type checking with a good type system. It's basically a safety net for redesigning: if my current idea doesn't work well, I can completely flip over the way my data is laid out, and the compiler will patiently point me to all the bits of code that need to change accordingly. Saves brain resources for more meaningful tasks.
Neo · 2022-09-15 13:39
>>32767 What about when you have to fight the type system but can't win? For example, I once wanted make some kind of deferred function evaluation queue in C++, but that would have meant being able to store the function pointer and the heterogenous argument list somewhere, which is only possible if you butcher your functions to take something like a void * vector, parse it inside the body and return void * too. Another problem I had: if I have a class template C<T> including fields or functions mentioning the T type, how do I dynamically access fields or dispatch functions between instances at runtime? Without duplicating all the code in switches, of course. See the example: template <typename T> class C { public: T fun(); T field; }; int main(void) { C<int> ci; C<char> cc; enum class Ctype {INT, CHAR} ctype = Ctype::INT; auto & fref = ACCESS_FIELD(ctype, field); auto ret = CALL_METHOD(ctype, fun); } Happened at work with an Image class template having Grayscale, RGB and CMYK as parameter. CALL_METHOD can be done easily with a switch in macro easily, but ACCESS_FIELD looks impossible to me. Which is why I think gradual typing is the way to go.
Linda Lee · 2022-09-15 13:57
>>32768 Said bullshit, here's the final form, basically, impossible to create a reference to an unknown type (outside the switch). #define CALL_PROCEDURE(type, name, ...) \ do \ { \ switch (type) \ { \ case Ctype::INT: ci.name(__VA_OPT__(__VA_ARGS__)); break; \ case Ctype::CHAR: cc.name(__VA_OPT__(__VA_ARGS__)); break; \ } \ } \ while (0) template <typename T> class C { public: T fun() {}; void proc(T arg) {}; T field; }; int main(void) { C<int> ci; C<char> cc; enum class Ctype {INT, CHAR} ctype = Ctype::INT; ACCESS_FIELD(ctype, field, ref_var); CALL_FUNCTION(ctype, fun, ret_var); CALL_PROCEDURE(ctype, proc, 0); }
HELIOS · 2022-09-15 17:31
gotos indexes that start at 1 negative indexes >>32755 should be pretty easy to emulate
Joey · 2022-09-17 12:02
>>32768 Well, with anything less than static typing, you have to do some of the compiler's job yourself after all. I prefer to leave theorem proving to automated theorem provers and use my brain for other tasks, myself - it certainly helps me focus. >For example, I once wanted make some kind of deferred function evaluation queue in C++, but that would have meant being able to store the function pointer and the heterogenous argument list somewhere, which is only possible if you butcher your functions to take something like a void * vector, parse it inside the body and return void * too. So, at arbitrary points in your code, you have a function that you're ready to call right there but would rather have executed later, right? For example, you could use it to mimic Go's defer operator, and maybe defer an object cleanup routine right after initializing it, for good practice. If so, I think that providing the function's arguments should be the responsibility of the call site, not the queue's, so I'd wrap the call in a thunk or something equivalent. That way, all deferred procedures take the same input type. Then there's the return value. Some procedures will return nothing meaningful. Others will, but even in that case, the queue itself won't have anything useful to do with that value, so I think in this case you'd want to register a callback. So, a deferred procedure is either (unit -> unit) or a sequence of a procedure of (unit -> 'a) and a callback of ('a -> unit) (that is, a thunk that returns anything, and a procedure that takes that same kind of anything and returns unit). The second case can be turned into the first simply by chaining the procedures together, the chief problem being the wordiness and boilerplate of wrapping your calls into thunks for deferral. To help with this, I'd give my queue two push procedures: one that takes (unit -> unit) directly, and another that takes a procedure and a callback as separate arguments, and does the wrapping internally. For the separate one, a little bit of polymorphism would ensure I can call it on procedures of any type, just as long as, in each pair, both procedures share the same intermediate type. An implementation follows. I wrote two: one uses plain thunks, the other exploits OCaml's lazy operator for hopefully lighter syntax (but I'm terrible at writing terse OCaml). ocaml module DefQueue = struct type dfq = (unit -> unit) Queue.t let create = Queue.create;; (* Simply add a pre-made thunk to the queue. *) let add q proc = Queue.add proc q;; (* Add a procedure and a callback to the queue. * "'a." means "for any type 'a" and makes the procedure polymorphic. *) let add_with_callback : 'a. dfq -> (unit -> 'a) -> ('a -> unit) -> unit = fun q proc callback -> let thunk = (fun () -> proc () |> callback) in add q thunk;; let run q = Queue.iter (fun thunk -> thunk ()) q; Queue.clear q;; end;; module LazyQueue = struct type lzq = (unit lazy_t) Queue.t let create = Queue.create;; let add q lz = Queue.add lz q;; let add_with_callback : 'a. lzq -> 'a lazy_t -> ('a -> unit) -> unit = fun q lz callback -> let lz' = lazy (Lazy.force lz |> callback) in add q lz';; let run q = Queue.iter (fun lz -> Lazy.force lz) q; Queue.clear q;; end;; let () = let dq = DefQueue.create () in let lq = LazyQueue.create () in print_endline "got some stuff to defer"; DefQueue.add dq (fun () -> print_endline "1: a literal thunk"); DefQueue.add_with_callback dq (fun () -> "2: a string with a callback") print_endline; DefQueue.add_with_callback dq (Fun.const 17) (fun n -> Printf.printf "3: the number %d\n" n); print_endline "would like to run that stuff now"; DefQueue.run dq; print_newline (); print_endline "got some stuff to procrastinate on"; lazy (print_endline "1: a lazy procedure call") |> LazyQueue.add lq; LazyQueue.add_with_callback lq (lazy "2: a string with a callback") print_endline; LazyQueue.add_with_callback lq (lazy 17) (fun n -> Printf.printf "3: the number %d\n" n); print_endline "enough lazying about for now"; LazyQueue.run lq;; And of course the output is: got some stuff to defer would like to run that stuff now 1: a literal thunk 2: a string with a callback 3: the number 17 got some stuff to procrastinate on enough lazying about for now 1: a lazy procedure call 2: a string with a callback 3: the number 17 Regarding your other case, the thick C++ parlance makes it hard for me to decipher, but I'll give it a spin. Clarifications welcome.
Wintermute · 2022-09-17 12:15
>>32744 Lack of function overloading, lack of c++ oop.
Dozer · 2022-09-17 13:59
>>32768 1. >c[++] >type system 2. if you ever feel like you're fighting the type system, or any other tool, you're doing it right and your tool is broken. 3. deferred and async crap is horse soykaf garbage and cannot be logically implemented anyway
Nobody special · 2022-09-17 15:23
>>32801 >Well, with anything less than static typing, you have to do some of the compiler's job yourself after all. I don't see why gradual typing is any less good. You can keep it completely static, if you want to. >queue Sorry, can't read ML and even less OCaml. If I understand well, you wrap functions with arguments into lambdas with no params/return value (unit -> unit, I guess unit <=> void) to have a homogenous type to store; and chain functions with return values with their callback to satisfy that type. Ingenious, though it's flirting with dynamism, as the lambda is created/allocated at runtime, right? Did it in C++, for the "fun" (aka not fun, because it looks like hell, even without the std:: pepper and because I ran into https://stackoverflow.com/questions/55992834/why-cant-argument-be-forwarded-inside-lambda-without-mutable https://stackoverflow.com/questions/52182420/variadic-template-perfect-forwarding-of-integer-parameter-to-lambda and had to use "mutable" for the first time). Goes to show how much more elegant ML is. #include <cstdio> #include <queue> #include <functional> #include <utility> using namespace std; class lambda_queue { private: queue<function<void ()>> q; public: template <typename F, typename... Targs> void push(F fun, Targs&&... args) { q.push([=]() mutable { return fun(forward<Targs>(args)...); }); } template <typename F1, typename F2, typename... Targs> void push_with_callback(F1 fun, F2 callback, Targs&&... args) { q.push([=]() mutable { return callback(fun(forward<Targs>(args)...)); }); } void pop() { q.front()(); q.pop(); } void pop_all() { while (!q.empty()) { pop(); } } }; int main(void) { lambda_queue lq; lq.push([](){ printf("Procedure\n"); }); lq.push(printf, "Function %d\n", 2); lq.push_with_callback( [](int arg){ printf("Function with callback\n"); return arg * arg; }, [](int arg){ printf("Callback %d\n", arg); }, 3 ); lq.pop_all(); } But I'll be honest, I do call that fighting the type system, as the intuitive/elegant way of doing it is to store the function along with its argument list, and not Rust tier type wrapping. Here's the same in CL (with an external queue package to be realistic): (load "queue.lisp") (defpackage #:lambda-queue (:use #:cl) (:shadow push pop)) (in-package #:lambda-queue) (defclass lambda-queue () ((queue :initform (queue::make) :accessor queue))) (defmethod push ((obj lambda-queue) fun &rest args) (queue::push (cons fun args) (queue obj))) (defmethod push-with-callback ((obj lambda-queue) fun callback &rest args) (queue::push (cons (lambda (&rest args) (funcall callback (apply fun args))) args) (queue obj))) (defmethod pop ((obj lambda-queue)) (destructuring-bind (fun . args) (queue::pop (queue obj)) (apply fun args))) (defmethod pop-all ((obj lambda-queue)) (loop while (not (queue::empty-p (queue obj))) do (pop obj))) (let ((lq (make-instance 'lambda-queue))) (push lq (lambda () (format t "Procedure~%"))) (push lq #'format t "Function ~D~%" 2) (push-with-callback lq (lambda (arg) (format t "Function with callback~%") (* arg arg)) (lambda (arg) (format t "Callback ~D~%" arg)) 3) (pop-all lq)) For the second "challenge", can't really explain it as it's probably more a C++ problem than a programming one.
John Titor · 2022-09-17 15:30
>>32803 >3. deferred and async crap is horse soykaf garbage and cannot be logically implemented anyway Was to implement "costly" logging in a real time system, so that the main thread doesn't wait for time getting, potential time formatting and fprintf. I can also imagine some other situations like a thread producing a queue of crap that must be each remotely sent (or any function that blocks for ages, basically). I mean, even sh's pipeline is "async crap".
if · 2022-09-17 16:02
>>32804 Scratch that, thunk are more elegant. Still, the intuitive solution isn't really possible with static typing, so there's simply no choice to have. Any other situation where dynamic tuples would be needed (i.e. where thunks can't be used as band-aids)?
Cat Mother · 2022-09-17 16:31
>>32772 GOTOs and 1 based indexing are fuarrrking cancer and luckily stayed dead
Junichi Fujiwara · 2022-09-18 12:33
>>32804 >I don't see why gradual typing is any less good. You can keep it completely static, if you want to. That's your answer right there - it being optional maintains the illusion that some parts of your program don't need to be type-correct. I speak as someone who loves Ruby and writes more of it than OCaml: even seemingly small and simple programs eventually reveal type errors in them, because the possible shapes data can take as it flows are too complex for my brain to model. The more I get used to static typing, the better my programming experience becomes. >If I understand well, you wrap functions with arguments into lambdas with no params/return value (unit -> unit, I guess unit <=> void) to have a homogenous type to store; and chain functions with return values with their callback to satisfy that type. That's exactly correct. You can indeed think of unit as the 'nothing' type. Strictly, it is the type that only contains one value, and since you can't encode any information if all you ever have is the same one value, well, there you go. >it's flirting with dynamism, as the lambda is created/allocated at runtime, right? I should expect so, yes. The lazy approach may do something more clever, but I haven't looked up the semantics. >But I'll be honest, I do call that fighting the type system The way I see it, the only thing the type checker can do for me in this scenario is to guarantee that a callback can handle any of its associated procedure's possible outputs, and it's doing that alright, but this is a matter of perspective. Now as for the other 'challenge' (this is coming from >>32768 for those playing along at home), indeed it's very C++-idiomatic, but one thing I did grasp is that you're using dynamic dispatch - calling a function that has many implementations, and having the correct one picked for you. OCaml doesn't have that, so I mulled over what a convenient way to achieve the same effect would be, and I came up with a rough sketch. This is something that no one asked for and most readers should skip the rest of this post, but I'm adding it for whatever worth it may have. Firstly, to replace your class template, I'd use a functor. A functor is 'a function from modules to modules', i.e., a piece of code that says 'if only I had some code that satisfied this interface, then I could do all these nifty things with it', and then you feed into it such a piece of code, and it gives you back code that does nifty things with it. I'd have a functor for image operations, and feed it modules for each format's implementation details, and that'd give me concrete modules for working with each format. But each of these modules is a separate object, and there's no inheritance or any other way in which they point to each other, so how do you make sure the right one is used according to the type of image object you have? What I'd try here is to wrap all my possible image formats into one sum type: type image = | Grayscale of grayscale_type | RGB of rgb_type | CMYK of cmyk_type The routines that create image objects would return them wrapped in this type, and so my code would be all happy working on a single type all the time. When the time came to call some of the format-specific routines in the modules, I'd pattern match to dispatch to the correct module: let some_operation image = match image with | Grayscale gs -> GrayscaleImage.some_operation gs | RGB rgb -> RGBImage.some_operation rgb | CMYK cmyk -> CMYKImage.some_operation cmyk And if I had to do this a lot, and since modules are first-class objects, I could have a single dispatch procedure that did this pattern matching and returned the appropriate module (plus the unwrapped image object to work on), and every other procedure would just call this one: let dispatch = function | Grayscale gs -> (module GrayscaleImage), gs | RGB rgb -> (module RGBImage), rgb | CMYK cmyk -> (module CMYKImage), cmyk let some_operation image = match dispatch image with | (module M), img -> M.some_operation img
↑ · 2022-09-18 13:16
>>32810 >That's your answer right there - it being optional maintains the illusion that some parts of your program don't need to be type-correct. I'll be honest, I understand the idea that gradual typing certainly makes technical debt easy to create and that it's basically compiler-checked specification (which you end up putting in documentation in fully dynamic PLs anyway). ML/Haskell type people tend toward the ideal of type theory: a program described in its entirety by types and type combinations. And the industry love it as powerful Indian checking. But when I work alone on my projects, I simply don't need static typing straight away and even encounter some (admittedly rare) situations where it could hurt; well, especially since C++'s type system is a joke compared to ML. NB: I talk about gradual typing in the context of SBCL, which has a powerful type inference system and very thorough warnings >The way I see it, the only thing the type checker can do for me in this scenario is to guarantee that a callback can handle any of its associated procedure's possible outputs, and it's doing that alright, but this is a matter of perspective. What I meant, is that there's an "intuitive" solution, the one your mind goes for first. But you're forced to abandon it because it wouldn't be possible. It's not "fighting", but it's definitely a narrowing of choice; which would probably remove the better solution in other problems than this one. >Image... Can the some_operation wrapper work if some_operation doesn't have the same return type in all the branches? Because that's the crux of the problem, at least in C++.
Crash Override · 2022-09-19 02:42
>>32744 Restarts Being able to decide how to recover from an error in one place, while having code closer to the errorful source provide the mechanism on how to recover from the error. Like a parser library signalling there was an invalid value, but also advertising that it can substitute in an alternate value, or skip parsing that section entirely and continue later in the file.
if · 2022-09-19 11:23
>>32816 >decide how to recover from an error in one place >having code closer to the errorful source provide the mechanism on how to recover I'd read about restarts, but I hadn't grasped what their point was until now. That sounds like it should solve a number of the problems with exceptions. >>32811 >Can the some_operation wrapper work if some_operation doesn't have the same return type in all the branches? Because that's the crux of the problem, at least in C++. Fascinating. You can in fact have a function whose return type depends on its input types, using generalized algebraic data types - I've played with that, it's quite cool. But this wrapper is calling functions from various modules, which have all been generated by the same functor and therefore must adhere to the same type signature. Can you use GADTs to make a module signature flexible such that functions form different implementations can return different types? I don't know right now, but I'll try to find out. Of course, what usually happens in ML is that the answer is way more obvious than it looks and the problem can be solved with plain old sum and product types - but since I don't know the full details of your problem domain, and since this exercise is helping me so much in grapsing the finer points of OCaml's type system, I will proceed down the GADT road. >But when I work alone on my projects, I simply don't need static typing straight away [...]. This is where our experiences simply differ. Me, I spent a long time writing writing brittle, misdesigned, unmaintainable scripts for my own use, and when I finally started to get a decent grasp of programming I quickly grew sick of my programs breaking on me. For a while I employed defensive programming obsessively, and what allowed me to keep my sanity were the concept of design by contract and static typing with ADTs. To me, static typing is liberating, because it provides a straight, low-cognitive-load path toward the correctness that I crave.
Cat Mother · 2022-09-19 18:33
>>32817 >To me, static typing is liberating, because it provides a straight, low-cognitive-load path toward the correctness that I crave. Can't say I don't understand. Anyone knows that approach of using static typing to its fullest so that you can progress mindlessly toward the "it compiles = it works" state; basically moving the complexity from the rest of the program to the type hierarchy and design. Still prefer gradual, as it really embodies the "premature optimization is the root of all evil" saying from Knuth (even if typing isn't just about performance); though type inference can make static typing much less painful. Just take care to not fall into Haskell type theory masturbation - the equivalent of C guys obsessing over micro optmization or Scheme academics and unreadable call/cc (effect handlers in OCaml?) worship. PS: hyped for multicore OCaml 5?
Tank · 2022-09-20 09:56
Raw pointers and pointer arithmetic. Float types.
quote · 2022-09-21 16:37
trigraphs. ??=include <stdio.h> int main(void) <% int a??(2??) = <%~0, 0%>; // ;) ???/ a<:0:> = 0; printf("a is %d\n", a??(0??)); %>
x<->y · 2022-09-22 09:40
>>32772 >should be pretty easy to emulate Sure. But in The Real World, when there's no direct syntactic support for a feature, when it's just a design pattern, there's no guarentee that other people will use it. When you consider that one of the main uses of namespaces is working with libraries written by other people, it's kind of a problem when they're not the default.
James K · 2022-09-22 13:14
>>32772 >indexes that start at 1 >negative indexes Neither of these things are the worst, but both at once makes no sense. Starting the index at one is saying that indices shouldn't be offset based, but labelling the back from -1 is extending the concept of offset from index 0 to say that a[-1] is implicitly a[|a| - 1]. With a negative index and 0 based: a[i] = a[i % |a|], but it you are going to say everything is one based: a[i] = if i < 0 then a[|a| - i + 1] else a[i]. These concepts just make no sense together.
Batou · 2022-09-25 09:39
>>32811 >Can the some_operation wrapper work if some_operation doesn't have the same return type in all the branches? Because that's the crux of the problem, at least in C++. I gave this a spin. I struggled with a type scope error for a while, and eventually gave up when it dawned on me that it'd be pointless anyway: if the different implementations return different types (which by itself compiled just fine), then they can't be used interchangeably in the same code; if different code paths are needed for each implementation, then you've given up dynamic dispatch. I don't rule out the possibility that the type scope errors were ultimately caused by this, as in my experience seemingly stupid type errors always eventually point you to a flaw in your logic. >>32819 >PS: hyped for multicore OCaml 5? I don't follow the news. I looked it up after reading your post, and well, I'm glad OCaml is finally catching up to, what, the 2010s at best? Incidentally, the one thing I did know was coming to OCaml and hyped about were effect handlers. Reading up more on them now, I see they're basically type-checked continuations and thus not particularly easy to grasp or use, but with the kind of benefits they promise, I'll have to give them a chance or two. I've noticed, after all, that there are only two kinds of concepts in the broader ML world: concepts that I used to think were too abstract and convoluted for a human to grasp but now know are actually very simple and silly, and concepts that are too abstract and convoluted for a human to grasp.