The Artima Developer Community
Sponsored Link

Weblogs Forum
The fate of reduce() in Python 3000

119 replies on 8 pages. Most recent reply: Dec 18, 2018 1:39 AM by Claudemir Moliane

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 119 replies on 8 pages [ « | 1 2 3 4 5 6 7 8 | » ]
Logan Hanks

Posts: 4
Nickname: loganhanks
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 9:05 AM
Reply to this message Reply
Advertisement
I would like to try to make a stronger case for preserving lambda, or at least the feature it provides, in all future versions of Python. I see lambda as being Pythonic (as far as I understand that term), and not as a functional programmer's crutch grafted onto the language when Guido wasn't looking. I also disagree that Guido has provided any strong, valid argument at all for removing lambda. I think, ultimately, that the flaws lambda currently has are all correctable, and there is no good reason to remove anonymous inline functions from the language.

There is no good definition of Pythonic, as far as I know. However, I do think I have some vague notion, which includes the following qualities:

The programmer knows best and is the ultimate authority.
Python doesn't make us declare types. Why? Because no one wants to dictate to other programmers how to write their code. Python hardly makes us do anything, and that's one reason why it's so great.
There is no reason why programmers should not be trusted to work with anonymous callables.

Power should be maximized.
If there is something worth doing in Python, there is usually a way to do it. When there isn't, there's usually at least a PEP out there proposing a new way to do it. Python has all sorts of hooks and features in order to give additional power to the programmer, power that often gives programmers new ways to violate many tenets held by long-time users of the language (see http://www.python.org/peps/pep-0020.html and consider features like operator overloading, metaclasses, list comprehensions and so forth).
The possibility of writing ugly code with anonymous callables is no justification for removing such a feature.

Conciseness is a virtue.
In Python, if something is worth doing, then there is almost always a way to write generalized code that allows you to minimize the expression of that something. Also consider that Python does not require programmers to name every single subexpression they use.
Support of anonymous callables is all about conciseness.

Consistency is a virtue.
All previous points could be said to be shared with other languages, such as Perl. This point makes Python stand out. It is possible to be expressive and creative with ease, even if syntax is restrictive.
If lambda is inconsistent, then it would be better to find a more consistent way to express the feature than to remove it altogether.

Guido has failed to provide any meaningful argument against lambda itself. If the syntax is confusing to functional programmers, then change the syntax. That is no reason to drop the feature. Who cares if there's more than one way to define a callable? That's far better than having zero ways to define an anonymous callable.

Guido's reasons, so far, seem founded in disrespect for the programmer. If being obvious and never confusing were necessary requirements for a feature to remain in Python, then I could list a whole host of features more politically correct than lambda that would also fail to qualify.

Finally, what is most galling is that Guido is dead wrong about there being no use for lambda. Sure, it is used with Tkinter, and it is used in toy programs. It is also used in very serious work.

Any event-driven programming benefits enormously from anonymous callables. In event-driven programming, callbacks are everywhere. Such programs react to other programs or to users themselves, and the complexity of these reactions ranges from trivial to incredibly complex. Python is so well-liked in this domain because the features it provides make all such levels of complexity manageable. In my own event-driven code I make heavy use of anonymous callables, as well as less disputed features such as introspection.

Event-driven programming is important. If you look at where Python is being most widely used, you'll probably find it used in scripts (is this what Guido means by "toy" programs?), networking, and user interfaces, and the latter two are where event-driven programming dominates. Look at any GUI toolkit (not just Tkinter), and look at any robust networking suite (e.g., http://twistedmatrix.com/).

In conclusion, I hope to see a lot more debate on this issue. Let us separate the issue of removing particular functions (such as map and reduce, which can always be provided by modules) from the issue of removing anonymous callables (which require language support). Just because Guido doesn't have a use for lambda does not mean that no one else does. The best compromise would be to see improvements to anonymous callable support in Python, rather than elimination. Anonymous callables belong in Python, and so far the arguments against this notion do not seem very Pythonic to me.

Jayson Vantuyl

Posts: 7
Nickname: kagato
Registered: Jan, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 9:21 AM
Reply to this message Reply
Here are some random thoughts on the above:

1. any/all are a great idea. They should be included REGARDLESS of removing lambda, map, filter, and reduce.

2. I think you're (Guido) right about requiring a PEP to remove these. Personally, I would remove them from the language proper, BUT perhaps give them a home in the standard library. Maybe include a Python (or maybe C) implementation of them in a "functional". I think that as time goes on, there are a number of things that could go there. A car/cdr-style list implementation (or lazy lists built on them) would be one that is both apropos and possibly efficient for certain applications.

The above also has the advantage of "porting" most existing code with "from functional import ...". The only downside is that there really is no way to seamlessly substitute for lambda.

Also, I have not checked to make sure there isn't already a third party library named "functional". Don't want to step on any toes there...

3. I support the new syntax proposed for filtering, i.e. (x in S if x > 42). I think this is a smashing idea and will make things less redundant in a lot of my code.

4. I think saying "reduce hurts" isn't really an answer. I have a few use cases. I'm not sure if they're justified, but I find it quite useful here.

I am parsing MGCP, which can piggyback multiple packets onto each other. When constructing a return packet, I sometimes need to piggyback, and I have made my class to do this:

bigpacket = packet1 + packet2

This returns a special compound packet object that "knows what to do". It is also expressive and obvious what's going on here. However, later in my code, I have to piggyback certain responses with certain commands programmatically. In this case, it is really useful to be able to say:

jumbo = reduce(operator.add,list_of_packets)

Similarly, I also have an application involving successively setting bits in a bit field. In this case, I have a list of bits to set in a persistent state variable. I use it as:

newstate = reduce(operator.or_,set_bits,oldstate)

Rather than:

newstate = oldstate
for bit in set_bits:
newstate |= bit

I'm not sure which is more readable, but you get the idea. There is a specific concept encapsulated by reduce that must be written out in longhand. Whether or not that's good, I'm not so sure.

5. Can we please get a short circuiting ternary operator? The and/or construct doesn't work. I would really like to be able to write (if c then a else b). There are just too many times when:

if ERROR:
ret = BAD
else:
ret = GOOD

could be expressed as:

r = GOOD if ERROR else BAD

-or-

r = if ERROR then GOOD else BAD

-or-

r = ERROR ? GOOD : BAD

It is possible to simulate the above with a function, but we can't short circuit without your help!

6. In response to the "anonymous callables" argument of Logan Hanks, I would point out that nested functions are, for all practical purposes, the same thing.

It is common to think of:

def func:

as defining a specific function at a specific place. In most languages, it is. These languages need "anonymous functions" so that they can treat functions more like data, a reference to be passed about. In Python,

def func:

is really a lot like

func = lambda(...)

for all practical purposes. As an example, examine the power of the following:

>>> def addn(n):
... def add(x,n=n):
... return x + n
... return add
...
>>> a5 = addn(5)
>>> a5(2)
7
>>> a5(6)
11
>>> alist = [ addn(x) for x in range(10) ]
>>> alist
[<function add at 0x8106b24>, <function add at 0x80dabfc>, <function add at 0x8135c1c>, <function add at 0x8135c54>, <function add at 0x8135c8c>, <function add at 0x8135ce4>, <function add at 0x8135d3c>, <function add at 0x8135d94>, <function add at 0x8135dec>, <function add at 0x8135e44>]
>>> alist[2](7)
9

Show me a lambda and I'll show you a nested function. They are equivalent.

Logan Hanks

Posts: 4
Nickname: loganhanks
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 10:24 AM
Reply to this message Reply
In response to Jayson Vantuyl:

I'm well aware that lambda and def create the same object. The difference is that lambda allows you to do it inline and without a name. The motivation for having such a feature is the same as for having a ternary operator: to reduce unnecessary naming.

The naming of concepts is great, and any non-trivial construct should be named. But often we deal with trivialities. In some cases in Python, we have to name trivialities (such as in certain if-else situations), which introduces a rather unnecessary cognitive load as well as clutter into the program. Thanks to lambda, we can anonymize some trivial callable definitions (though unfortunately we're still limited to only callables implemented by a single expression). Rather than seeing this feature removed, I would prefer to see it improved.

Steven Bethard

Posts: 1
Nickname: bediviere
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 10:42 AM
Reply to this message Reply
Guido van Rossum wrote:
> BTW I definitely expect having to defend removing
> map/filter/reduce/lambda with a PEP

I hope that when this PEP comes about someone will gives examples of how to translate common lambda uses into Python 3000 code. Worth pointing to is a little data analysis I did of the standard library:

http://mail.python.org/pipermail/python-list/2004-December/257990.html

I found that a fair number of examples were:

(1) not actually using lambda to declare an anonymous function and could just as easily have been a def statement,

(2) rewriteable with operator.* functions,

(3) rewritable with list comprehensions/generator expressions, or

(4) rewritable with bound methods of an instance

Nonetheless, there were a fair number of examples that I couldn't find a clean way of rewriting, and I'd like to see at least a few of them addressed in a PEP.

That said, I'm all for removing map, filter, reduce and lambda. I read Guido's statement:

"almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do."

and immediately thought "Amen!" =)

Christopher Eckman

Posts: 1
Nickname: eck
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 12:20 PM
Reply to this message Reply
I don't use reduce but I definitely use map, filter and lambda. Asides from the arguments others proposed for lambda staying -which I agree with- I also feel map and filter lead to a more elegant (and readable) solution than list comprehension in certain cirumstances.

I'd rather them not go away but if they do (and it is appearing they will) I'd much rather them go to a "functional" module of the type Guido was proposing than to disappear altogether.

I do like the any and all suggestions and did like the other suggestions about car, cdr, first, last for a functional module.

Merriodoc Brandybuck

Posts: 225
Nickname: brandybuck
Registered: Mar, 2003

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 6:51 PM
Reply to this message Reply
TAL,

key, value = map(str.strip, line.split('=', 1))

can also become

key, value = [param.strip() for param in line.split('=',1)]

I still think map is a bit cleaner, but you keep the one-lineness of it. That being said, I'll miss map if it goes. I do use that a lot in my own code and it is, I believe, a rather clean way to do what it does. I find it more readable then list comps most of the time.

The others (lambda, reduce, filter) I don't use much, so I won't miss much, which isn't to say I think they should go.

As for any() and all(), I can see that they are pretty useful. I originally wasn't that fired up about them because of the exception that Guido noted, but then I thought about how I actually use this sort of thing. The one example that immediately came to mind was seeing if all the characters in a string are valid hex values. That would look something like this



>>> def all(S):
... for x in S:
... if not x:
... return False
... return True
...
>>> print hexvals, validchars
['3', '9a4', '234r45', 'af343b', 'xxyy'] 0123456789abcdefABCDEF
>>> for h in hexvals:
... if not all([d in validchars for d in h]):
... print h
...
234r45
xxyy
>>>



In most of the cases I would really use something like any() or all(), I wouldn't care about the variables scoped inside all. I'm sure other will, but for me, my original reservation, based on my use, turned out to be unfounded. Which is nice :-)

And Bill, you can use [python][/python] tags for formatting, too.
 almost does the trick, but not quite :-)

Kay Schluehr

Posts: 302
Nickname: schluehk
Registered: Jan, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 11, 2005 9:14 PM
Reply to this message Reply
> I ran into the above-mentioned problem with sum() over
> non-numbers just recently.

> I think sum() should just add
> the elements in the sequence instead trying to add 0 all
> the time when i'm not specifying a starter value. In
> other words, these should work:
>
> sum(['foo']) -> 'foo'
> sum(['a', 'b']) -> 'ab'
> sum([[2], [3]]) -> [2, 3]
>
> The current implementation of sum() fails on all these
> examples. Its behaviour seems unnecessarily restricted to
> me.

Yes, for shure. sum() seems to be nothing more than an optimization hack for numbers not a replacement of the polymorphic reduce(). On the contrary I would extend reduce() to deal with generators, stop-conditions and fixed-point semantics. The advice better to take "".join() for strings should vanish from docs for a wiser implementation:


def sum(sequence):
if isinstance(sequence[0],str):
return "".join(sequence)
elif isinstance(sequence[0],(int,long,float)):
return superhacks._my_slick_number_adder(sequence)
elif hasattr(sequence[0],"__add__"):
_sum = sequence[0]
for summand in sequence[1:]:
_sum+=summand
return _sum
raise TypeError,"Can't sum up sequence"



The idea of a "startvalue of a sum" is an implementation detail, not a mathematical idea. Forcing the "startvalue" behind all other summands reminds me on reversing the digits in a byte within an address coding. I'm somewhat traumatized...

Regards Kay

Kay Schluehr

Posts: 302
Nickname: schluehk
Registered: Jan, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 12, 2005 2:19 AM
Reply to this message Reply
> I would like to try to make a stronger case for preserving
> lambda, or at least the feature it provides,
> in all future versions of Python. I see
> lambda as being Pythonic (as far as I
> understand that term), and not as a functional
> programmer's crutch grafted onto the language when Guido
> wasn't looking. I also disagree that Guido has provided
> any strong, valid argument at all for removing
> lambda. I think, ultimately, that the flaws
> lambda currently has are all correctable, and
> there is no good reason to remove anonymous inline
> functions from the language.

It is not an inline function, it is a lambda :)

Instead of whining why don't you propose as a compromise an "inline" keyword as a replacement for the half-assed "lambda"?

Replace

func(x,y, lambda n: int(n != 1))

by

func(x,y, inline def f(n): return int(n!=1) )

Use of "inline" wouldn't be any pretentious and actually be consistent with other notions used in python. Obviously this construct can be extended to classes as well.

Regards Kay

James Thiele

Posts: 1
Nickname: jamesth
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 12, 2005 8:26 AM
Reply to this message Reply
Why does reduce() have to die?
Guido writes:
the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly.

It seems to me that this implies that reduce() is only used to apply buit-in operators - I disagree so here's an example from graph theory.

Consider a directed acyclic graph represented by a list of 2-tuples, where the first element of the tuple is the name of the node and the second element is a set of the nodes that have arcs from the node to them. For example:

>>> Graph = [
>>> (1, set((2, 3))),
>>> (2, set((4,))),
>>> (3, set(())),
>>> (4, set((6,))),
>>> (5, set((6,))),
>>> (6, set(()))
>>> ]

If we want to find all the nodes reachable from a node we can define a function:

>>> def reachable(source, node):
>>> if node[0] in source[1]:
>>> source[1].update(node[1])
>>> return source

Note that reachable() is an associtive operator. Use it with reduce():

>>> source = reduce(reachable, Graph)
>>> print source
(1, set([2, 3, 4, 6]))

The equivalent loop is:

>>> source = Graph[0]
>>> for node in Graph[1:]:
>>> source = reachable(source, node)
>>> print source
(1, set([2, 3, 4, 6]))

Is this really better than reduce()?

Marcin Kowalczyk

Posts: 40
Nickname: qrczak
Registered: Oct, 2004

Re: The fate of reduce() in Python 3000 Posted: Mar 12, 2005 10:03 AM
Reply to this message Reply
The sum of an empty sequence of numbers is 0.

Python doesn't distinguish an empty list of numbers from an empty list of strings.

Conclusion: polymorphism doesn't work for operations performed on an arbitrary number of arguments when the arbitrary number can be 0. That is, sum can't be defined for non-numeric values without breaking its behavior for an empty sequence.

Well, there is an optional second argument of sum, for the starting value. If sum was to work for non-numbers, it should require the second argument in these cases - then it's OK. Making bare sum(['a','b','c']) working would be a recipe for disaster when someone later supplies an empty list of strings.

Arthur Siegel

Posts: 1
Nickname: artisa
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 12, 2005 11:29 AM
Reply to this message Reply
Glad 'sum' came up in the context of this discussion. It immediately comes to mind to me in the general context here - but I thought it might just be me.

what is suggested

sum(['foo']) -> 'foo'
sum(['a', 'b']) -> 'ab'
sum([[2], [3]]) -> [2, 3]

what is:

>>> sum(['foo'])

Traceback (most recent call last):
File "<pyshell#14>", line 1, in -toplevel-
sum(['foo'])
TypeError: unsupported operand type(s) for +: 'int' and 'str'

It concerned me then (when sum was suggested and accepted), and concerns me more now that Numeric3 and its goals have been announced, that sum would be considered without regard to its Numeric meaning.

>>> import Numeric as N
>>> N.sum(N.array('foo'))
'foo'
>>> N.sum(['foo'])
array([f , o , o ],'O')
>>> N.sum('foo')
'foo'

>>> N.sum(N.array(['a','b']))
'ab'
>>> N.sum(N.array('a','b'))
97
>>> N.sum(['a','b'])
'ab'

>>> N.sum(N.array([[2],[3]]))
array([5])
>>> N.sum([[2],[3]])
array([5])


Suggesting to me that the means of normalization with Numeric3 is not necessarily apparent, but that if one takes the goals of Numeric3 seriously, something that probably be considered in thinking about the future of the current Python built-in.

Art

Joel Neely

Posts: 10
Nickname: joelneely
Registered: Mar, 2003

Re: The fate of reduce() in Python 3000 Posted: Mar 13, 2005 6:24 AM
Reply to this message Reply
> The idea of a "startvalue of a sum" is an implementation
> detail, not a mathematical idea.
>

To borrow a phrase, "that's not even wrong". The reason that a "startvalue of a sum" is zero for addition is that zero is the identity for addition, which *IS* most definitely a mathematical idea. There are *MANY* associative operations (whether symmetric/commutative or not) commonly used in programming that could usefully be applied to a collection of values:


Operation Identity

addition 0
multiplication 1
set union empty set
set intersaction universe (for current domain)
boolean and true
boolean or false
bitwise and all bits on
bitwise or all bits off
maximum least element (for current domain)
minimum greatest element (for current domain)
... ...


Therefore, any higher-order function which means "take this operation and apply it 'between' consecutive values of that collection" must have access to the identity of the operation, both as a "kick-start" value to begin the chain of applications, and as the appropriate response for an empty collection.

The nice thing about properly-designed higher-order functions is precisely that they *DON'T* have assumptions (e.g. about what value to use for the identity of a chained operation) that limit their usability. In fact, the principle of being able to interoperate reasonably with code that is written later (even if unforeseen) is one of the hallmarks of good OOP and FP. If someone invents a new type (and we do that all the time in defining new classes) with a reasonable chaining operation and identity, a pre-existing application-chaining HOF is very useful!

In fact (avoiding the "r" word ;-), if "apply X from Z to Vs" is viewed as equivalent to "result = Z; for vv in Vs: result = result X vv; return result" then it isn't even necessary for X to by associative (and Z only needs to be a left identity).

It sad to see misconception (the statement I'm replying to) and hostility (a previous post's reference to coding for readability by "normal people") being injected into what could be an objective (and mathematical) discussion of language design. We programmers are supposed to be able to learn new things all the time; why is a little learning for such potentially-huge payoff so onerous?

Eric Nieuwland

Posts: 1
Nickname: mcernie
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 13, 2005 12:27 PM
Reply to this message Reply
Being the compatriot Guido mentioned I feel I might as well add to the discussion here ;-)

> Therefore, any higher-order function which means "take
> this operation and apply it 'between' consecutive values
> of that collection" must have access to the identity of
> the operation, both as a "kick-start" value to begin the
> chain of applications, and as the appropriate response for
> an empty collection.

That's why I think this stuff should be moved from the builtins to a library package. We might even add some protocol to aid in a pretty implementation.

> In fact (avoiding the "r" word ;-), if "apply X from Z to
> Vs" is viewed as equivalent to "result = Z; for vv in Vs:
> result = result X vv; return result" then it isn't even
> necessary for X to by associative (and Z only needs to be
> a left identity).

If you want to process data structures with an inherent ordering (list, tuple) you are right. However, I think the things Guido talks about should also apply to sets, hence associativity (and reflexivity should probably be added).

Kay Schluehr

Posts: 302
Nickname: schluehk
Registered: Jan, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 13, 2005 1:36 PM
Reply to this message Reply
> > The idea of a "startvalue of a sum" is an
> implementation
> > detail, not a mathematical idea.
> >
>
> To borrow a phrase, "that's not even wrong". The reason
> that a "startvalue of a sum" is zero for addition is that
> zero is the identity for addition, which *IS* most
> definitely a mathematical idea.

The neutral element of some group is not the "start" for anything. A "sum" is usually chosen for some commutative operation ( but it need not be in the context of Python. String concatenation is a counterexample ). Furthermore an addition can be defined on algebraic structures, without any neutral element e.g. the semigroup {x|x is even and x>=2}. or N+ the set of positive integers. Funny enough, the Roman numbers have no 0 too. It was invented by the Arabs. Poor old Romans, Greeks and Egyptians ... ;)

The addition should depend on the structure it is acting upon. Creating such structures and offering an infix "+" is possible in Python due to custom classes and overloading __add__. If this structure defines a neutral element of addition it can be used as a default value for sum-operations allthough i prefer None or an exception if the sum has no arguments ( +1 for an exception from my side ).

[...]
> Therefore, any higher-order function which means "take
> this operation and apply it 'between' consecutive values
> of that collection" must have access to the identity of
> the operation, both as a "kick-start" value to begin the
> chain of applications, and as the appropriate response for
> an empty collection.

???

1+1 = 2

Where do You see the 0? Is there one hidden in the sum? It is obviously an "implementation detail" to read the sum from left to right or right to left ( if You are Hebrew You probably prefer the latter direction ). You can also start in the middle if there is some welldefined "middle" and the addition commutates. The concept of a function in set-theory abstracts completely from the process of "summation". One would implement it as an infinitely big lookup table, where the complete relation is mapped :)

IMHO the "startvalue"-thing is nothing but the iterator way of thinking which may fit the brains of python-dev but is a bit suspicious for the algebra crowd.


> The nice thing about properly-designed higher-order
> functions is precisely that they *DON'T* have assumptions
> (e.g. about what value to use for the identity of a
> chained operation) that limit their usability.

Yeah.

> In fact,
> the principle of being able to interoperate reasonably
> with code that is written later (even if unforeseen) is
> one of the hallmarks of good OOP and FP. If someone
> invents a new type (and we do that all the time in
> defining new classes) with a reasonable chaining operation
> and identity, a pre-existing application-chaining HOF is
> very useful!

You can try to implement category theory in Python and specify everything up to isomorphism. This could lead to a full flegded DSL or even better a complete computer algebra system ( possible without leaving the language? ) I like this way of thinking but i suspect Your boss is merciless with those guys who differ to much from the average and though not being a masochist i have some comprehension for this point of view. The purposeful existence of a "king" a BDFL who represents the symbolic middle should not be
underestimated.

Regards Kay

Gary Robinson

Posts: 1
Nickname: garyrob
Registered: Mar, 2005

Re: The fate of reduce() in Python 3000 Posted: Mar 13, 2005 2:52 PM
Reply to this message Reply
I would greatly miss lambda, though I don't care about the others. One really convenient use of lambda, very frequent in my code, is in defining properties:


iCount = property(lambda self: len(self.stUsers))


I think that this is very readable, and that the value in this example really should be a property. A method could be defined that was used in the property definition, but using lambda is much more concise and eliminates the need to introduce a supporting method.

Also, if the property definition refers to a method, you have to go read the method to know what the property is. Here's it's self-contained and so easier to read.

Flat View: This topic has 119 replies on 8 pages [ « | 1  2  3  4  5  6  7  8 | » ]
Topic: A Hazard of Covariant Return Types and Bridge Methods Previous Topic   Next Topic Topic: Markdown vs. AsciiDoc

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use