Implicit conversion to boolean in if and while statements

M

Michael Torrie

He is so accustomed to "guessing" that it has become second nature
for him.

I think most of us are guessing as to what you're talking about since
you're responding to a 7 month old thread that I think most people have
long since deleted from their e-mail or nntp readers.

Gotta hand it to you, though. It's working.
 
R

Rick Johnson

Because it makes it simple to distinguish between having an object and
not having one without having to explicitly test for it each time.

That's a strange thing to say when you go on to provide an example that tests the validity of the object "each and every time":
db = connect("my:db") # or None if the connection failed
if db:
<do something>
I find that usage to be incredibly intuitive.

And i find it to be incredibly asinine.

Consider this:

if connect("my:db") as db:
<do something>

No need to make a call and then test for the validity of the call when you can do both simultaneously AND intuitively.

*school-bell-rings*
 
R

Rick Johnson

I think most of us are guessing as to what you're talking about since
you're responding to a 7 month old thread that I think most people have
long since deleted from their e-mail or nntp readers.

Well i know this thread is quite old however i never got a chance to provide a response due to heavy work loads. I feel this subject is very important for the python language, hence my late replies.
 
R

Rick Johnson

I think most of us are guessing as to what you're talking about since
you're responding to a 7 month old thread that I think most people have
long since deleted from their e-mail or nntp readers.

Well i know this thread is quite old however i never got a chance to provide a response due to heavy work loads. I feel this subject is very important for the python language, hence my late replies.
 
S

Steven D'Aprano

Rick said:
Why even have a damn bool function if you're never going to use it?

bool is for converting arbitrary objects into a canonical True or False
flag. E.g. one use-case is if you wish to record in permanent storage a
flag, and don't want arbitrary (possibly expensive) objects to be recorded.

Most of the time, you shouldn't care whether you have a canonical True/False
bool, you should only care whether you have something which duck-types as a
boolean flag: a truthy or falsey value. In Python, all objects duck-type as
flags. The usual interpretation is whether the object represents something
or nothing:

"nothing", or falsey values: None, False, 0, 0.0, '', [], {}, set(), etc.
(essentially, the empty value for whichever type you are considering)

"something", or truthy values: True, 1, 2.5, 'hello world', etc.
(essentially, non-empty values).

Prior to Python 3, the special method __bool__ was spelled __nonempty__,
which demonstrates Python's philosophy towards duck-typing bools.
 
T

Thomas Rachel

Am 08.02.2013 07:29 schrieb Rick Johnson:
Consider this:

if connect("my:db") as db:
<do something>

No need to make a call and then test for the validity of the call when you can do both simultaneously AND intuitively.

Would be great, but can be emulated with

def ifiter(x):
if x: yield x

for db in ifiter(connect("my:db")):
<do sth with db>

Is not very intuitive, however, but does its job.


Thomas
 
S

Steven D'Aprano

Really Rick? Digging out a post from nearly seven months ago? You must
really be bored silly.
 
S

Steven D'Aprano

Rick said:
GvR has always been reluctant to incorporate full OOP machinery for some
reason.

Python is a fully object oriented language. It is *more* object oriented
than, say, Java.

- everything in Python is an object, there is no distinction between "boxed"
and "unboxed" variables;

- modules are objects;

- functions and methods are objects;

- classes are objects in Python, and have their own class (the metaclass);

- metaclasses themselves are also objects, and have classes of their own;

- it's objects all the way down, at least until you reach "type" itself,
which is bootstrapped into existence by the compiler.


Although Python is fully object-oriented, it does not insist on one
particular style of object syntax. It allows procedural and functional
style syntax as well.

I am not suggesting that Python be 100% OOP, HELL NO! But
collections should have had an "isempty" method from the beginning. But
the same argument could be made against len, any, all, etc...

No they shouldn't.

http://effbot.org/pyfaq/why-does-py...ndex-but-functions-for-other-e-g-len-list.htm

http://lucumr.pocoo.org/2011/7/9/python-and-pola/

http://mail.python.org/pipermail/python-dev/2008-January/076612.html


Python functions operate as *protocols*. any() and all(), for example, are
excellent examples of why your suggestion fails: the principle of "Don't
Repeat Yourself".

In Python today, any() and all() will work perfectly on ANY ITERABLE OBJECT,
for free. The developer of that object doesn't need to do anything to
support any() and all(), all she has to do is make it iterable.

Under your suggestion, every iterable object has to implement an any()
method, and an all() method. Every iterable type has to repeat the same old
code as every other iterable type.

class list:
def any(self):
for x in self:
if x: return True
return False

class dict:
def any(self):
for x in self:
if x: return True
return False

class set:
def any(self):
for x in self:
if x: return True
return False

class MyFancyIterableThatIsRealCool:
# Wow, deja vu...
def any(self):
for x in self:
if x: return True
return False


See all the pointlessly duplicated code? Now each one needs tests, and
documentation, and the amount of duplication goes through the roof.

Now, a developer of merely average intelligence will see all that duplicated
code, and factor it out into a global function (two actually, one for
any(), one for all()):

def any(iterable):
# Write this once. Test it once. Document it once.
for x in iterable:
if x: return True
return False

class list:
# Gotta have a method, or Rick will cry.
def any(self):
return any(self)

class dict:
def any(self):
return any(self)

class set:
def any(self):
return any(self)

class MyFancyIterableThatIsRealCool:
# This code seems trivial, and familiar...
def any(self):
return any(self)


But a developer of above average intelligence will recognise that all those
x.any() boilerplate methods are *pointless and stupid*, since you have a
function that does everything you need, for every possible iterator, for
free. All you need do is use any(obj) syntax instead of obj.any() syntax,
which also saves one keystroke.

And a *really* smart language designer will have realised this ahead of
time, and designed the language to encourage the use of protocols like
this, instead of insisting on the slavish application of obj.method syntax.
 
M

MRAB

Rick said:
Why even have a damn bool function if you're never going to use it?

bool is for converting arbitrary objects into a canonical True or False
flag. E.g. one use-case is if you wish to record in permanent storage a
flag, and don't want arbitrary (possibly expensive) objects to be recorded.

Most of the time, you shouldn't care whether you have a canonical True/False
bool, you should only care whether you have something which duck-types as a
boolean flag: a truthy or falsey value. In Python, all objects duck-type as
flags. The usual interpretation is whether the object represents something
or nothing:

"nothing", or falsey values: None, False, 0, 0.0, '', [], {}, set(), etc.
(essentially, the empty value for whichever type you are considering)

"something", or truthy values: True, 1, 2.5, 'hello world', etc.
(essentially, non-empty values).
Anything that's not falsey is truey.
Prior to Python 3, the special method __bool__ was spelled __nonempty__,
which demonstrates Python's philosophy towards duck-typing bools.
Incorrect, it was spelled __nonzero__.
 
R

Rick Johnson

Python is a fully object oriented language. It is *more* object oriented
than, say, Java.

Oh really? *chuckles*
- everything in Python is an object, there is no distinction between "boxed"
and "unboxed" variables;

Just because /everything/ in Python is an object does not mean that Python is 100% OOP. This fact is just one of the many attributes of a 100% OOP language. Yes, Python allows OOP style, but python is NOT 100% OOP! Ruby on the other hand /is/ 100% OOP. Although it has identity issues like Python. Ruby thinks it's multi-paridigm and Python thinks it's a good example of OOP.Neither are correct.
- modules are objects;

- functions and methods are objects;

- classes are objects in Python, and have their own class (the metaclass);

- metaclasses themselves are also objects, and have classes of their own;

- it's objects all the way down, at least until you reach "type" itself,
which is bootstrapped into existence by the compiler.


Although Python is fully object-oriented, it does not insist on one
particular style of object syntax. It allows procedural and functional
style syntax as well.

Well you just defeated yourself. How can Python be 100% OOP and then allow other paradigms?
I am not suggesting that Python be 100% OOP, HELL NO! But
collections should have had an "isempty" method from the beginning. But
the same argument could be made against len, any, all, etc...

No they shouldn't.

[...]

Python functions operate as *protocols*. any() and all(), for example, are
excellent examples of why your suggestion fails: the principle of "Don't
Repeat Yourself".

In Python today, any() and all() will work perfectly on ANY ITERABLE OBJECT,
for free. The developer of that object doesn't need to do anything to
support any() and all(), all she has to do is make it iterable.

Under your suggestion, every iterable object has to implement an any()
method, and an all() method. Every iterable type has to repeat the same old
code as every other iterable type.

NOT IF PYTHON WERE TRULY 100% OOP!

If so, Python would have a supertype called "Collection" that wold define all methods that operate on collections. Some of these include:

len, any, all, length, isempty, __getitem__, __setitem__, etc...

Then any collection subtype would inherit from this supertype and get the methods for free.
[...]
See all the pointlessly duplicated code? Now each one needs tests, and
documentation, and the amount of duplication goes through the roof.

Now, a developer of merely average intelligence will see all that duplicated
code, and factor it out into a global function (two actually, one for
any(), one for all()):

Only if that developer does not understand sub-typing! All he has to do is write the method ONE TIME in a super-type, and then inherit the method intoANY number of sub-types for free. Now, if he wants to pervert the usage ofa method to fit some niche, THEN he will need to overload the method and provide proper return value.
But a developer of above average intelligence will recognise that all those
x.any() boilerplate methods are *pointless and stupid*, since you have a
function that does everything you need, for every possible iterator, for
free. All you need do is use any(obj) syntax instead of obj.any() syntax,
which also saves one keystroke.

And a *really* smart language designer will have realised this ahead of
time, and designed the language to encourage the use of protocols like
this, instead of insisting on the slavish application of obj.method syntax.

Using built-in functions to operate on objects is foolish because you are placing extra burden on the programmer to know which /functions/ work with which /types/. The *only* functions that should be global are the kind that will work on *ANY* object. But then again, the Object type could hold thesemethods!

len, all, and any (just to name a few) only work for collections types and as such should be methods of these types. The global functions:

sum, len, any, all, enumerate, map, max, min, reversed, sorted, zip

can only be applied to sequence types, or subtypes of a sequence type. So using a /real/ OOP paridigm we would do the following:

## START TRUE OOP PARIDIGM ##

class Object(SuperType):
def __class__
def __delattr__
def __doc__
def __format__
def __getattribute__
def __init__
def __new__
def __repr__
def __setattr__
def __sizeof__
def __str__
def __subclasshook__
def true? # aka: bool
def callable?
def compare(other)
def dir
def hash
def help
def id
def isinstance?(Type)
def issubclass?(Type)
def super
def type

class SequenceBase(Object):
# Methods from object are free
def __add__
def __contains__
def __delattr__
def __delitem__
def __delslice__
def __eq__
def __ge__
def __getitem__
def __getslice__
def __gt__
def __iadd__
def __imul__
def __iter__
def __le__
def __lt__
def __mul__
def __ne__
def __reduce_ex__
def __rmul__
def __setitem__
def __setslice__
def __subclasshook__
#
# Interface
#
def iterator
def length
def sum
def any
def all
def enumerate
def filter(proc)
def frozenset
def map
def max
def min
def reverse
def reduce(proc)
def slice
def sort
def zip


class MySequencyThing(SequenceBase):
# do something here


class List(SequenceBase):
# Methods from SequenceBase and Object are free!
#
# Interface
#
def append
def count
def extend
def index
def insert
def pop
def remove
def reverse
def sort


class MyListyThing(List):
# do something here

## END TRUE OOP PARIDIGM ##

You see, 100% OOP uses encapsulation, inheritance, sub-typing, etc, etc... But most fundamental to OOP is interface (methods belonging to objects), not global functions applied to objects in some haphazard fashion that some self-appointed dictator pull from his backside due to his fear of true OOP style!

Python is not 100% OOP. Heck you cannot fairly apply a specific percentage level because Python's level of OOP is defined by each user of the language.. The best way to describe Python is as promiscuous language who secretly longs to be 100% OOP, and to fulfill this fantasy it cross-dresses in OOP lingerie on the weekends.
 
R

Roy Smith

Rick Johnson said:
The best way to describe Python is as promiscuous language who secretly
longs to be 100% OOP, and to fulfill this fantasy it cross-dresses in OOP
lingerie on the weekends.

+1 QOTD :)
 
R

Rick Johnson

[...]

So using a /real/ OOP paridigm we would do the following:

## START TRUE OOP PARIDIGM ##

[...snip naive example...]

Actually my example API is littered with artifacts of a python "global function architecture". In a /true/ 100% OOP language most of these "dunder" methods would become interface members of the object.

There is also the question of WHEN to use and WHEN NOT to use the "dunder" naming convention. I actually like the convention myself for clearly defining methods that are called by "syntactic sugar". However, python employs the convention quite haphazardly.

For example:
__iadd__ is called by an expression such as: "1+=1"
which is translated into: "1.__iadd__(1)"

However:
__repr__ is called by the the global "repr" function
which is translated into: "obj.__repr__()"

I don't like the second usage because i believe this naming convention should be reserved for syntactical sugar only. But i digress...

Here is a better example of Python converted into true OOP paridigm (renaming and removing methods appropriately to fit my logical 100% OOP API).

class Object(SuperType):
def construct # aka: __init__
def desconstruct # aka: __del__
def class
def delattr(name)
def doc
def getattr(name)
def __new__ # dunder???
def repr
def setattr(name, value)
def size
def stringify # aka: __str__
def subclasshook # XXX: dunder???
def true? # aka: __bool__
def callable?
def compare(other)
def methods
def instance_methods
def hash
def help
def id
def isinstance?(this)
def issubclass?(this)
def super
def type

class SequenceBase(Object):
# Methods from object are free
def __add__
def __contains?__
def __delitem__
def __delslice__
def __eq__
def __ge__
def __getitem__
def __getslice__
def __gt__
def __iadd__
def __imul__
def __iter__
def __le__
def __lt__
def __mul__
def __ne__
def __rmul__
def __setitem__
def __setslice__
#
# Interface
#
slice = __getslice__
extend = __add__
contains? = __contains?__
def length # pka: __len__
def any
def all
def enumerate -> iterator
def filter(proc)
def map(proc)
def max
def min
def reverse
def reduce(proc)
def sort
def zip


class Sequence(SequenceBase): # aka: list
# Methods from SequenceBase and Object are free!
#
# Interface
#
def append(this)
def count(this)
def index(this)
def insert(idx, this)
def pop()
def remove(this)
def reverse
def sort

I'm a bit unnerved by the sum function. Summing a sequence only makes senseif the sequence in question contains /only/ numeric types. For that reasoni decided to create a special type for holding Numerics. This will probably result in many complaints from lazy people who want to use only one Sequence type, which holds mixed types, THEN jamb nothing but numeric types intoit, THEN have a sum method that throws errors when it encounters a non-numeric type!!! I say, too bad for you.

Stop obfuscating your code! Of course someone could probably find a legitimate reason to apply a sum method to non-numeric values; if so, then inheritfrom NumericSequence and create your custom type!

class NumericSequence(Sequence):
# Methods from Sequence, SequenceBase, and Object are free!
def __setitem__(item):
if not item.isinstance(Numeric):
raise TypeError()
def __add__(other):
if not other.isinstance(NumericSequence):
raise TypeError()
def __setslice__(other):
# blah
#
# Interface
#
def sum -> Integer
 
S

Steven D'Aprano

MRAB said:
On 2013-02-08 07:22, Steven D'Aprano wrote:
Incorrect, it was spelled __nonzero__.

Oops, so it was. Sorry for the brain-fart.

__nonzero__ or not, nevertheless the implication still applies: all types
are meant to map to "nothing" (zero) or "not nothing" (non-zero).
 
C

Chris Angelico

I'm a bit unnerved by the sum function. Summing a sequence only makes sense if the sequence in question contains /only/ numeric types. For that reason i decided to create a special type for holding Numerics. This will probably result in many complaints from lazy people who want to use only one Sequence type, which holds mixed types, THEN jamb nothing but numeric types into it, THEN have a sum method that throws errors when it encounters a non-numeric type!!! I say, too bad for you.


Most assuredly not. The sum builtin works happily on any sequence of
objects that can be added together. It works as an excellent flatten()
method:
nested_list = [["q"], ["w","e"], ["r","t","u"], ["i","o","p"]]
sum(nested_list,[]) ['q', 'w', 'e', 'r', 't', 'u', 'i', 'o', 'p']
nested_list
[['q'], ['w', 'e'], ['r', 't', 'u'], ['i', 'o', 'p']]

I'm not sure what your definition of a numeric type is, but I suspect
that list(str) isn't part of it.

ChrisA
 
R

Rick Johnson

The sum builtin works happily on any sequence of objects
that can be added together. It works as an excellent
flatten() method:
nested_list = [["q"], ["w","e"], ["r","t","u"], ["i","o","p"]]
sum(nested_list,[]) ['q', 'w', 'e', 'r', 't', 'u', 'i', 'o', 'p']
nested_list
[['q'], ['w', 'e'], ['r', 't', 'u'], ['i', 'o', 'p']]

What the hell? Oh yeah, you must be using pike again. No, if it were pike the list would look like this:

({({"q"}), ({"w","e"}), ({"r","t","u"}), ({"i","o","p"})})

Of course you'd have to declare it first using an /expanded/ Java syntax:

nested_list = array(array(string))

Folks, i couldn't make this stuff up if i wanted to. Go read for yourself if want a few laughs.

http://pike.lysator.liu.se/docs/tutorial/data_types/container_types.xml
I'm not sure what your definition of a numeric type is, but I suspect
that list(str) isn't part of it.

Of course not.
 
R

Rick Johnson

The sum builtin works happily on any sequence of objects
that can be added together. It works as an excellent
flatten() method:
nested_list = [["q"], ["w","e"], ["r","t","u"], ["i","o","p"]]
sum(nested_list,[]) ['q', 'w', 'e', 'r', 't', 'u', 'i', 'o', 'p']
nested_list
[['q'], ['w', 'e'], ['r', 't', 'u'], ['i', 'o', 'p']]

What the hell? Oh yeah, you must be using pike again. No, if it were pike the list would look like this:

({({"q"}), ({"w","e"}), ({"r","t","u"}), ({"i","o","p"})})

Of course you'd have to declare it first using an /expanded/ Java syntax:

nested_list = array(array(string))

Folks, i couldn't make this stuff up if i wanted to. Go read for yourself if want a few laughs.

http://pike.lysator.liu.se/docs/tutorial/data_types/container_types.xml
I'm not sure what your definition of a numeric type is, but I suspect
that list(str) isn't part of it.

Of course not.
 
I

Ian Kelly

I'm a bit unnerved by the sum function. Summing a sequence only makes sense if the sequence in question contains /only/ numeric types. For that reason i decided to create a special type for holding Numerics. This will probably result in many complaints from lazy people who want to use only one Sequence type, which holds mixed types, THEN jamb nothing but numeric types into it, THEN have a sum method that throws errors when it encounters a non-numeric type!!! I say, too bad for you.

Stop obfuscating your code! Of course someone could probably find a legitimate reason to apply a sum method to non-numeric values; if so, then inherit from NumericSequence and create your custom type!

Are you aware that the approach you're advocating here is bad OOP
design? If you declare a class NumericSequence with the property that
it contains only numeric types, and then you declare a subclass
NonnumericSequence that does not share that property, then guess what?
You've just violated the Liskov Substitution Principle.

The goal you're trying to achieve here is nonsensical anyway. Ask
yourself what the semantic meaning of the sum() function is, what
purpose it is meant to serve. My answer: it is the reduction of the
addition operator. The implication of this is that the input type of
the sum() function is not "numbers", but rather "things that can be
added". That includes numbers, but since I see from your proposed
class hierarchy that you are retaining the __add__ method on
sequences, it also includes sequences. Are you really going to tell
the user that (1, 2, 3) + (4, 5, 6) is perfectly fine, but that the
semantic equivalent sum([(1, 2, 3), (4, 5, 6)]) is nonsense?
 
C

Chris Angelico

The sum builtin works happily on any sequence of objects
that can be added together. It works as an excellent
flatten() method:
nested_list = [["q"], ["w","e"], ["r","t","u"], ["i","o","p"]]
sum(nested_list,[])
['q', 'w', 'e', 'r', 't', 'u', 'i', 'o', 'p']
nested_list
[['q'], ['w', 'e'], ['r', 't', 'u'], ['i', 'o', 'p']]

What the hell? Oh yeah, you must be using pike again. No, if it were pike the list would look like this:

({({"q"}), ({"w","e"}), ({"r","t","u"}), ({"i","o","p"})})

Of course you'd have to declare it first using an /expanded/ Java syntax:

nested_list = array(array(string))

Strange... normally I have to actually bring Pike up, no idea why
you're doing it for me. But okay.

Actually, that's not a declaration, that's an assignment; and in Pike,
a 'type' is a thing, same as it is in Python (though not quite). If I
were to declare it in Pike, it would be:

array(array(string)) nested_list;

Though the part inside the parens can be omitted, in which case the
array can contain anything, rather than being restricted to strings.
In actual fact, Rick, despite your complaints about the syntax, it's
able to achieve exactly what you were thinking Python should do:
declare an array/list that contains only numbers.
Folks, i couldn't make this stuff up if i wanted to. Go read for yourself if want a few laughs.

http://pike.lysator.liu.se/docs/tutorial/data_types/container_types.xml

Apart from some syntactic differences, which in the scheme of things
are pretty trivial, the only difference is that Pike permits (without
demanding) you to restrict an array's members. In fact, Pike lets you
declare everything as 'mixed' if you like, allowing you to put
*anything* into *anywhere*... which, yaknow, is pretty much identical
to Python's model (only with declared variables), and takes advantage
of the fact that there are no Java-style "unboxed" types (the Pike int
is like the Py3 int or the Py2 long, arbitrary precision).
Of course not.

And yet, in both Pike and Python, adding lists/arrays of strings
together is not just legal but extremely useful. Do we need to rewrite
the sum() function to handle lists instead of letting operator
overloading take care of it for us? Suppose I make a string subclass
with a division operator:

class divisible_string(str):
def __truediv__(self,n):
n=(len(self)+n-1)//n
return [self[pos:pos+n] for pos in range(0,len(self),n)]
# and a bunch of functions so operations on a divisible_string return
a divisible_string

Now, I should be able to divide a string by 2 and get a two-element
list. Kinda convenient, would be cool if str supported this, but it's
OOP and OOP is all about operator overloading, even if it makes your
code less readable - a language isn't 100% OOP unless this sort of
thing is normal, right?

Suppose also that I have an avg() function that does this:

def avg(it):
it=iter(it)
tot=next(it)
pos=-1
for pos,val in enumerate(it):
tot+=val
return tot/(pos+2)

I can take the avg() of a list (or other iterable) of integers, and
get back a number. Do I need to rewrite this function to handle my
divisible_string? Or do I need to have divisible_string subclass some
"averageable" class/interface in order to permit them to be used
inside a list that has an avg() method?

lst=avg(map(divisible_string,("asdf","qwer","a","z","f","qqqqq","asdfasdfasdf")))

How do you solve the puzzle, Rick?

ChrisA
 
I

Ian Kelly

The sum builtin works happily on any sequence of objects
that can be added together. It works as an excellent
flatten() method:
nested_list = [["q"], ["w","e"], ["r","t","u"], ["i","o","p"]]
sum(nested_list,[])
['q', 'w', 'e', 'r', 't', 'u', 'i', 'o', 'p']
nested_list
[['q'], ['w', 'e'], ['r', 't', 'u'], ['i', 'o', 'p']]

What the hell? Oh yeah, you must be using pike again. No, if it were pike the list would look like this:

({({"q"}), ({"w","e"}), ({"r","t","u"}), ({"i","o","p"})})

Of course you'd have to declare it first using an /expanded/ Java syntax:

nested_list = array(array(string))

Folks, i couldn't make this stuff up if i wanted to. Go read for yourself if want a few laughs.

Classic ad hominem. Try to make your opponent look bad by making fun
of them on a completely unrelated topic, and then hope that nobody
notices that you entirely ignored the substance of their argument.
Sorry, it didn't work.

You didn't even do a good job of it. Yes, Pike uses two characters
instead of one to wrap array literals. Big friggin' whoop. On the
minus side, it's a little more typing. On the plus side, they stand
out better, and you don't have the [] characters doing double duty
denoting list literals and indexing alike. Yes, Pike writes **string
as the more readable array(array(string)) -- although if my memory
serves correctly the former is also legal syntax. Again, nobody
cares. And by the by, Pike is a descendant of C, not Java. Its
predecessor LPC predates Java by about 6 years. If you're going to
start lambasting others' preferred programming languages in an effort
to make yourself look good, you can at least get your facts straight.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,756
Messages
2,569,535
Members
45,008
Latest member
obedient dusk

Latest Threads

Top