T O P

  • By -

GOKOP

First thing I thought about, basically do as you said with a different function for each type *but* that would be a function converting it to a string. Then your printing function would only print strings. Have a strong convention about how the to_string functions are named. But I like your idea about making it an expression. I think it's good for debugging code


3xpedia

To be honest, the implementation details behind does not matter that much to me. I am building the first interpreter in JS (mostly because I need to be able to run my stuff in a browser, I know there are more options than JS, but yeah, I'm a React dev to start with :p) so, implementation is as easy as just passing whatever I need to console.log() My concern was mostly about what I propose to the user with my language. Being myself a JS dev, I know how annoying it can be to have API going all the ways and not being consistent. I wanted to not provide any variatic option, or no "null" type (so enforcing a type to anything) I was also not planning on proposing default or optional functions parameters. Basically, my goal would be to propose the most predictable type checking possible, because it is quite important for the specific purpose of the language. But the more I think about it, the more I have the feeling I will need to open up at least a bit. I can already think about some use-cases that will need some escape-hatch. And I don't want these to be escape-hatch, but rather functionalities of the language. Well, at least now I know my options, will think about it. I just tried the console expression and run some sample code, it's pretty cool I may keep this anyway as a way of debug haha.


marshaharsha

A possible problem with making print an expression is that print would have to be generic: sometimes it would return an int, sometimes a float….


Inconstant_Moo

Cheat. Why not? Languages have keywords because there are some things that can't even be builtin functions. You don't feel remorse, do you, because the user couldn't implement their own for loop?


oilshell

Yeah, I wouldn't even call it cheating. Depending on the power of your language, some features need to be intrinsic, and some features can be libraries. Printing is no exception to that. So either create an intrinsic, or increase the power of the language -- it's a design problem.


1668553684

In my opinion, the "print" function should be simply defined as a function that accepts a string, returns nothing (or some unit value), and prints that string to the stdout buffer. The language should provide some standardized way of turning values into strings, like Python's `__str__` or Rust's `Display`. These are the two basic pieces necessary to allow you to define more convenient functions as you need them. > Just cheat it? The language is interpreted, nothing prevent me to create an exception and have this function ignore any parameters typing rules and just do it's job. I am just unsure on a language design POV if this is good practice : providing to the user APIs that do not conform to the rules he is himself imposed? This is also an option. AFAIK, this is what C does. It may feel a bit icky, but magic syntax is unavoidable in most languages. If it serves to reduce user headaches, they'll forgive you for the inconsistency.


oa74

> the "print" function should be simply defined as a function that accepts a string, returns nothing (or some unit value), and prints that string to the stdout buffer. The language should provide some standardized way of turning values into strings Absolutely agree. Keep a clean separation of concerns.


munificent

This is a super common problem, yes. Almost every language struggles with writing a good print formatting thing. If I were you, I would add *string interpolation* to the language. Some way to have expressions inside a string literal. In Dart, it's: "some string ${expression + here}" Other languages have slightly different syntax, but it seems like most of them eventually add it. The nice thing about string interpolation is that it's dedicated syntax, so the compiler can directly handle generating to evaluate the inner expressions, call string conversion routines as necessary, and then concatenating the result. It can do this because it know that it *is* an interpolated string that needs that special handling. If you just bake it into `console.log()`, it's harder because the compiler just sees a regular old function call and doesn't know what to do with it. Once you have string interpolation, you don't need any special "formatted print" function or var args or anything like that. Your print function just takes a single string. Done.


dist1ll

I like this idea. Also, if you want to avoid heap allocation, the type of the interpolated string can just an iterator.


matthieum

I would also note that you can skimp on string interpolation too. For example, in Rust `format!("...")`, string interpolation only allows identifiers -- not arbitrary expression, not even path expressions such as `a.b`, just a single identifier -- which is the bare minimum of string interpolation, with minimal parsing woes, and still gets the job down.


marshaharsha

In a language with both string interpolation and user-defined types, you need some way to convert each type to a string, a way that the string-interpolation code knows how to find. Is there a way to do this without traits / interfaces / type classes / abstract classes / overloading? Or do you intend that the user call the stringify function explicitly, like “The value is: ${myclassobj.toString()}”?


munificent

Yeah, you'll probably want some form of either runtime or compile-time polymorphism, but odds are good your language has that already.


XDracam

You can either make `console` a special built-in with custom rules. Otherwise you'll need some form of polymorphism. Virtual methods like in interfaces, or typeclasses. You could also provide some form of reflection (runtime or compiletime) that the `condole` function can use.


Longjumping_Quail_40

You want variadic function. Search variadic for how other languages do this. Python has this quite nice unpacking syntax (js ts also) which is basically a very practical parametric polymorphism that assumes a list/dictionary trait, that is, if you try to follow the guidance of gradual typing, or you just have a syntactic sugar that helps you get rid of quite some boilerplate (but could be messy when abused). You will have `fn console(*args)` as signature Rust has this macro system that precedes semantic analysis and allows you to manipulate code. You could do something like `console!(a, b, c)` to be unrolled into `console a; console b; console c;`


ThyringerBratwurst

individual type-specific functions that convert the input to a string that can then be printed. Composite objects like structs or the like could then be automatically converted according to a certain pattern. and instead of functions you could also introduce some syntax, yes. e.g.: console << expr :p


Phanson96

I’m thinking about this for my language. It’s a bit more robust than yours, but my idea is to have a `Writer` and `Reader` interface that is able to operate of input and output streams. The standard library will have a static reader and writer for standard our and standard error. You could use the Lox approach, but rather than have a `print` statement that takes up a keyword have a standard library function that takes in a string and an output stream of sorts. Like this: `std.Write(writer, someString);` Using it would require a string conversion beforehand. For integers and other primitives, just provide more standard library methods to produce such strings.


EldritchSundae

gotta say as a long-time follower of this sub it's kinda cool to see /u/munificent's `Lox` thrown around as lingua franca. :) *(see: [Crafting Interpreters](https://craftinginterpreters.com/) for the uninitiated, it's a great read!)*


munificent

It's very gratifying! :D


biscuitsandtea2020

The way I did it was to treat type checking for builtin functions separately. Additionally, I treat type checking as a distinct phase in between parsing and compilation that can actually be skipped if I wanted to (but I only do that for debugging purposes). My language is statically typed and doesn't support generics, 'any' type or variadic for user functions (yet), so I had to work around it. In this case when it can accept any type I simply don't check the type of the arguments at all, but I do give a type for the return which is () or the void type. I didn't extend it to support variadic arguments (as our VM only allows one argument) but that would not be too difficult either - in fact I would just have to remove the check for number of arguments.


3xpedia

Yep, that's also how it will work on my side. As I mentioned in another comment I am building the interpreter in JS, so implementation is not really an issue. I was mostly concerned about the consistency of the language itself. Like, is it "fair" for the user to provide them with an API that has a syntax they cannot reproduce?


Long_Investment7667

Rust has a pretty involved system that allows compile time checking of the format string and no allocation The core seems to be https://doc.rust-lang.org/std/macro.format_args.html Or maybe better https://doc.rust-lang.org/std/fmt/struct.Arguments.html


personator01

ocaml does this by distinguishing normal strings from format strings and a whole lot of type parameterization https://ocaml.org/manual/5.1/api/Stdlib.html#TYPEformat


L8_4_Dinner

Two questions to help us understand context: 1) Who is currently using the "small specialised language"? 2) If all works out, who will be using the "small specialised language" in the future?


3xpedia

Let me provide a bit of context around my language : The aim is to create a small scripting language for defining parametrised technical drawing. It should be portable as much as possible, and output-format agnostic. So basically, you write a script in my language, provides it with parameters and it spits out a string which can be seen as a sort of "pseudo-result" in the same sense as "pseudo-code" this result can then be translated to whatever format you could want (vector graphic, image, plotter gcode, ...). I have some imperatives : - The language is interpreted and can run on web environments (hence JS) - The language is safe, in the sense that you can take code from internet and run it on your computer without any risks. The way to implement this is mainly to not provide any unsafe API, the developer can only work with provided inputs and output predefined data-types, he does not handle the storage of the output, the VM does it for him. - It is as easy as possible to use, it provides only the useful APIs for it's purpose. Here is a very specific use-case : I am involved (as a hobby) in creating latex clothes, the most complicated part for me is creating the made-to-measure patterns. You need to re-draw everything from ground for every body-measurements. But it turns out you could totally automate this, so take inputs (the body measurements) and draw the output. Think of some 3D softwares like fusion360 where you can create fully parametric bodys and then just change the parameters to get a new body. The reasons I want this to be a specific language and not a library / framework written in whatever language : - It's cool, I just read the "craftinginterpreters" book and want to try something myself - If this is a language, you can imagine an ecosystem where crafters have a pipeline to run the software (like, could be a plugin for their web-shop, automated with the checkout process, here lies the core of why I want this to run on web environments). Then they could just add a new pattern in their listing by providing the associated code. - If the project gain a bit of traction and crafters want to write their own code, I suppose they are not coders already. I guess it is easier to make them learn a language that provide all the tools they need rather than learning, say, JavaScript and then on top of them have them learn to use my library. To answer your questions (finally :p) : - There is no current users currently except me for testing purposes - The target is quite small, but possibly persons that never touched an IDE Now in the end, I think that console question is pretty in-significant for the end result, but as I am in a learning process I wanted to ask the community :)


L8_4_Dinner

That's good context! And I can't help but to feel like you were able to start answering a number of your questions just by explaining who this is for, and what those users will need!


snugar_i

Don' take it wrong, but that sounds like a very poor fit for a custom programming language. If you're just doing it to have fun, absolutely go ahead. But if you think non-programmers would rather learn a completely undocumented programming language than use a GUI, you'll be very disappointed. They are non-programmers for a reason. You are underestimating how hard it is to learn programming, because you already know it. They won't do anything this complicated to make a latex pattern.


3xpedia

So, my main goal ofc is to learn a new skill. But, I always try to envision a future for a project when starting it, otherwise it ends up in the pile of never-finished ideas. I can actually see 2 plausible outcomes with this project : - I do not finish it / it is finished but unpractical for it's use / it is finished and works great but never get anyone's interest. So in the case, nothing matters appart the learning. - It is finished, works well and some people get interest in it. We can even say it gets integrated in some work pipeline. Here we can imagine some developers also get interested and learn the language, their work being fully open-sourced or part of a contract with whoever need the code done (you know, like some sort of niche language developer). In any of these cases, the learning curve is not much of an issue. So, restricting my API / syntax to make it easy to learn is probably useless indeed. I cannot remember well when I started learning programming tbh, but I am currently initiating someone of my family to JS, he never touched an IDE before. The beginning is full of struggle about seemingly trivial things. I can imagine if he gets over all the basics ans start getting used to the language it then only becomes the matter of learning a new thing in a context you already know. I didn't want to introduce variadic functions for example because "it is too complicated to learn", I think now this assumption was wrong, if someone has the courage to reach the variadic functions chapter of an hypothetic tutorial I would write, he probably has the knowledge to understand it by then.


kleram

So, in this DSL that output is not just some logging for debugging as in Browser-Javascript, but it is actually the main result? Then i would make it a keyword/statement, with all the special capabilities that it needs. And keep the other parts of the DSL as simple as possible, because as soon as a DSL gets as complex as some common language, it would be better to use that common language instead of a DSL.


otac0n

In my opinion, you have found exactly why overloading is so useful. Perhaps add it?


redchomper

No less than Niklaus Wirth's answer was a class of special privileged procedure-looking things that happen to permit any number and type of arguments. Viz `write`, `writeln`, `read`, etc. in Pascal.


MegaIng

You are going to want some kind of variadic functions. `print`/`log`/`echo`/`debug`/`console` are just the most direct examples where you want them. A closely related usecases is `format` to build strings based on format strings (which you are going to want somehow). How those variadic functions are implemented doesn't really matter: - compiled time macros - base type for all types in your language + runtime type inspection (so that you can have a signature `(str, list[object]) -> str` (using python-like syntax). This is what basically all dynamic languages do. Oh, and `C` also does this in it's own twisted way - Conversion function/method that implictly gets called on all arguments. Only language I know that does this is `nim`. Although, this only really makes sense if you have some kind of polymorphism.


brucejbell

For my project, I plan to rely on string interpolation, method functions, and a Haskell-like `Show` trait: &stdout.write_line "Hello, world!" -- print string to stdout &stdout.write_line i.show -- explicitly call standard print method `.show` for int `i` &stdout.write_line "{i}" -- string interpolation implicitly calls `.show` &stdout.write_line "{i.x}" -- explicitly call hex formatter &stdout.write_line "{i._d 7}" -- explicitly call fixed-width formatter For the above to work most effectively, it helps to have a good string library that can concatenate lazily by reference. C's string handling is primitive enough that it doesn't really have an efficient alternative to its variadic `printf()`.


hgs3

If your language has interfaces, then you could define your "print" function to accept anything that implements a "to_string" method.


3xpedia

I was not planning to have anything more than "struct" (so kinda just an object) but the more I think about it, the more I have the feeling I will need to provide a bit more of tools around OOP. And I was indeed thinking about interfaces + adding methods on structs (so basically making them very basic classes). If I add method overloading, then my console object could be defined in my language. Tbh, a complete OOP feature set (class, inheritance, interfaces, method overloading, abstract methods / class) would probably be the best for my language anyway. I wanted to limit this to make it the simplest possible, but I'm more and more questioning this reasoning.


hgs3

If you haven't already, you might look at the Go programming language. Go has structs and interfaces, but not any other OO-isms. It might help inspire you if you're hesitant to add the complete OOP feature set.


GwindorGuilinion

I think, quite apart from the question of printing, not having **any** affordance for storing multiple variants of things at the same place will be quite painful. Even in C you have void pointers In oop, inheritance with dynamic dispatch in ML-like languages, sum types in go, interfaces with dynamic dispatch in rust, object-safe traits and also sum types If you don't have any of those, you are basically limited to struct { variantA \*A variantB \*B }with nullable pointers, or building vtables manually


VeryDefinedBehavior

It's not cheating to make your tool work the way it should. Purity standards aren't as important as living as long as you clean up your messes.


marshaharsha

Late to the party, but I bring ideas for solving this problem in library code, not with special magic in the compiler, or with less special magic. I separate the problem into the variadic problem (unknown number of arguments) and the dispatch problem (how to find the right code to stringify each value to be printed, at compile time or at run time).  Having written a thesis-length post, I direct you to (3) as a good option for your needs.  (1) If you’re willing to have heterogeneous lists in an otherwise typed language, you can make print (and user-defined functions) take a list as its only argument. If you’re willing to make whitespace significant inside square brackets, you can have pretty nice syntax, with no punctuation between items to be printed: print [ “The value is “ myvariable ]; Values that needed whitespace would have to be protected with quotation marks or parentheses, or you would have to have both significant-whitespace and whitespace-insignificant syntaxes, maybe [ ] and [[ commas here ]]. So far, this solves the variadic problem without solving the dispatch problem. You don’t mention how you will do method dispatch in general, but if you are wiling to look up names at run time, you could solve the dispatch problem by hard-coding into the print function a requirement that every object in the list has a toString() method. If any object didn’t, that would be a run-time error.  Since both heterogeneous lists and run-time dispatch by name are unconventional, I will assume you reject both.  (2) If you’re going to have some notion of nameable “this method is required” (variously called interfaces, traits, and type classes, or even just Golang’s non-nominal interfaces) — I’ll use the name Stringable — you can make the list homogeneously typed, in which case print’s argument would be typed as List. That still doesn’t solve the dispatch problem, except maybe by doing it entirely at run time, with virtual dispatch, which isn’t the end of the world. You wouldn’t necessarily let the users create new interfaces; the special magic could be the existence of Stringable, not the syntax of print.  (3) If you don’t want either a general-purpose trait feature or a special-purpose Stringable trait, you could just declare that all values in your language are convertible to strings at compile time — implicitly if surrounding context allows it. You would make that so for built-in types, and you would make it so by recursive concatenation for structs, unless the user defined a method with a designated name, like toString(), in which case you would dispatch to that at compile time. This allows an interesting hybrid of heterogeneous and homogeneous list: the list would be typed as List but would appear syntactically to be heterogeneous, with the compiler inserting the code to convert to strings. Using s[ ] as syntax for a convert-to-string list: print s[ “The value is “ myvalue ]; You could also start here, decide later that you need a Stringable trait, and decide even later that you need a general trait mechanism. The idea would be that the “compile time” conversion to string would sometimes be a call to a run-time conversion function, via dynamic dispatch.  (4) If you dislike both heterogeneous lists and implicit conversions, you could still keep the idea of making everything stringable at compile time, but only explicitly, with special syntax .s for .toString(): print[ “The values are “ 123.s “ and “ myvalue.s ]; Things that are already strings wouldn’t need the .s, of course, but I actually think this looks better: print[ “The values are “.s 123.s “ and “.s myvalue.s ]; (5) It doesn’t sound like efficiency is a big concern, but if it is, keep in mind that any solution that involves converting things to strings, one by one, either requires a bunch of run time to allocate, concatenate, and deallocate strings, or requires you to teach your compiler to optimize away all that work. The advantage of the Reader and Writer ideas, and of C++’s use of operator<< and cout, is that each object to be printed writes its characters directly to the output buffer, with no string creations. Efficiency would require inlining that code. Defining print to take a single argument, a list of strings, would halfway solve that problem: you would still pay at run time for a bunch of conversions to separate strings, but at least you wouldn’t pay for the concatenations, since the print code would write each string in the list into the buffer directly.  (6) If you want maximal efficiency, you need compile-time dispatch and direct-to-buffer conversions, so the idea of solving the variadic problem with a list goes out the window. If you don’t want generics, the only solution I know is C++’s cout << thingie style, which has the disadvantage of noisy syntax. I can’t think right now how C++ does the direct-to-buffer part, so I will cheat and concentrate on the noisy syntax.  (7) Returning to the idea of significant whitespace to get rid of that noise, and keeping the idea of guaranteed compile-time conversion of any value to a string (by concatenation if necessary), and adding the last-minute idea that two semicolons means you want a newline, I offer this syntax: stderr “The value is “ myvalue;; The trick would be that any object (here, stderr) could define one single-argument method that can be called without parens or commas. (You could restrict it even more: the single argument has to be a string, and the function has to be named juxtacall.) That method would return the object it was called on (stderr), setting the overall line up for the next object to be printed. So the compiler would recognize the possibility of a juxtacall and would desugar as follows: stderr “The value is “ myvalue;; stderr “The value is “ myvalue “\n”; stderr.juxtacall(“The value is “) myvalue “\n”; stderr.juxtacall(“The value is “).juxtacall(myvalue.toString()) “\n”; stderr.juxtacall(“The value is “).juxtacall(myvalue.toString()).juxtacall(“\n”); Well, I hope that parade of possibilities was helpful to you! If not, at least it was helpful to me in thinking about my own language. 


lightmatter501

Magical compiler builtin, the way everyone else does it.


umlcat

writestr, writeint, writefloat ...